Compare commits

...
21 Commits
Author SHA1 Message Date
gdlbo ee1153b909 cached playlists dialog in settings 2024-03-18 17:40:25 +03:00
gdlbo 9e549079b4 fixed gzip 2024-03-18 17:13:05 +03:00
gdlbo 673660f610 reduced fetching thumbs 2024-03-18 16:20:32 +03:00
gdlbo 1865392a65 thumbs caching
also some catalog fixes
2024-03-18 16:09:42 +03:00
gdlbo d52d9f594c fixed playlist empty thumb crash 2024-03-18 14:22:28 +03:00
gdlbo b1fbe458f8 added count of tracks in playlist 2024-03-18 13:52:03 +03:00
gdlbo 4070ad3a03 fixed playlists files deletion 2024-03-18 13:39:28 +03:00
gdlbo 80c317120e Update CatalogJsonInjector.java 2024-03-18 13:21:32 +03:00
gdlbo c4cea050da Merge branch 'main' into playlist_caching 2024-03-18 10:38:46 +03:00
gdlbo 444887a191 playlist db remover 2024-03-18 10:14:32 +03:00
gdlbo 9bfc39c0f1 add to db from dl success 2024-03-18 09:55:40 +03:00
gdlbo 9ec443cde0 migration from old base 2024-03-18 09:47:38 +03:00
gdlbo 68270f8afb resolved conflicts 2024-03-18 09:16:03 +03:00
gdlbo b8d7330398 Merge branch 'main' into playlist_caching 2024-03-18 09:10:10 +03:00
gdlbo c79b419f5e init
WIP

1. Make migration from old base to new
2. Remove track with playlist (not only db deletion)
3. Make download one track add to db
2024-03-17 19:22:57 +03:00
Artur Skubei ba3f9b9afe Merge branch 'main' into playlist_caching 2024-03-17 14:26:37 +03:00
gdlbo 66e2ae5b93 conflicts resolved 2023-09-25 15:17:29 +03:00
gdlbo af2296e8ff Merge branch 'main' into playlist_caching 2023-09-25 15:05:42 +03:00
gdlbo 433811531a Merge branch 'main' into playlist_caching 2023-09-15 18:13:33 +03:00
gdlbo 0f5ccfd5ba fix empty playlist adding to offline catalog 2023-05-08 21:25:15 +03:00
gdlbo 38d775a538 init 2023-05-08 21:14:49 +03:00
24 changed files with 874 additions and 284 deletions
@@ -9,6 +9,7 @@ import com.vk.music.common.MusicPlaybackLaunchContext;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.VKXUtils; import ru.vtosters.lite.utils.music.VKXUtils;
@@ -19,18 +20,29 @@ import static ru.vtosters.hooks.other.Preferences.milkshake;
import static ru.vtosters.lite.utils.NetworkUtils.isNetworkConnected; import static ru.vtosters.lite.utils.NetworkUtils.isNetworkConnected;
public class MusicBottomSheetHook { public class MusicBottomSheetHook {
public static ArrayList<MusicAction> hook(final ArrayList<MusicAction> actions, final MusicTrack musicTrack) { public static ArrayList<MusicAction> hook(ArrayList<MusicAction> actions, MusicTrack musicTrack) {
if (musicTrack.F1()) return actions; if (musicTrack.F1()) return actions;
final var trackId = asId(musicTrack);
String trackId = asId(musicTrack);
if (isVkxInstalled()) actions.add(getPlayInVKXAction()); if (isVkxInstalled()) actions.add(getPlayInVKXAction());
if (isIntegrationEnabled()) { if (isIntegrationEnabled()) {
if (VKXUtils.isVkxCached(trackId)) actions.add(getRemoveCacheTrackVkxAction()); if (VKXUtils.isVkxCached(trackId)) {
else if (isNetworkConnected()) actions.add(addToCacheTrackVkxAction()); actions.add(getRemoveCacheTrackVkxAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackVkxAction());
}
} else { } else {
if (MusicCacheImpl.isCachedTrack(trackId)) actions.add(getRemoveCacheTrackAction()); if (MusicCacheImpl.isCachedTrack(trackId)) {
else if (isNetworkConnected()) actions.add(addToCacheTrackAction()); actions.add(getRemoveCacheTrackAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackAction());
}
} }
if (isNetworkConnected()) actions.add(downloadAsMp3Action()); if (isNetworkConnected()) actions.add(downloadAsMp3Action());
return actions; return actions;
} }
@@ -72,8 +84,12 @@ public class MusicBottomSheetHook {
} else if (isNetworkConnected()) { } else if (isNetworkConnected()) {
actions.add(addToCacheTrackVkxAction()); actions.add(addToCacheTrackVkxAction());
} }
} else if (isNetworkConnected()) { } else {
actions.add(addToCacheTrackAction()); if (PlaylistCacheDbDelegate.isCachedPlaylist(AndroidUtils.getGlobalContext(), playlist.v1())) {
actions.add(getRemoveCacheTrackAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackAction());
}
} }
if (isNetworkConnected()) actions.add(downloadAsMp3Action()); if (isNetworkConnected()) actions.add(downloadAsMp3Action());
@@ -134,10 +150,14 @@ public class MusicBottomSheetHook {
} }
public static boolean injectOnClick(int actionId, Playlist playlist) { // playlist inj public static boolean injectOnClick(int actionId, Playlist playlist) { // playlist inj
if (actionId == R.id.play_in_vkx) if (actionId == R.id.play_in_vkx) {
return tryPlayInVKX(null, null, playlist); return tryPlayInVKX(null, null, playlist);
}
if (actionId == R.id.add_to_cache) { if (actionId == R.id.remove_from_cache) {
PlaylistCacheDbDelegate.deletePlaylist(AndroidUtils.getGlobalContext(), playlist.v1());
return true;
} else if (actionId == R.id.add_to_cache) {
AudioDownloader.cachePlaylist(playlist); AudioDownloader.cachePlaylist(playlist);
return true; return true;
} }
@@ -147,7 +167,7 @@ public class MusicBottomSheetHook {
try { try {
service.deletePlaylistFromCache(playlist.a, playlist.b); service.deletePlaylistFromCache(playlist.a, playlist.b);
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.fillInStackTrace();
} }
}); });
return true; return true;
@@ -156,7 +176,7 @@ public class MusicBottomSheetHook {
try { try {
service.addPlaylistToCache(playlist.a, playlist.b, (playlist.Q != null) ? playlist.Q : ""); service.addPlaylistToCache(playlist.a, playlist.b, (playlist.Q != null) ? playlist.Q : "");
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.fillInStackTrace();
} }
}); });
return true; return true;
@@ -168,7 +188,7 @@ public class MusicBottomSheetHook {
try { try {
service.downloadPlaylist(playlist.a, playlist.b, (playlist.Q != null) ? playlist.Q : ""); service.downloadPlaylist(playlist.a, playlist.b, (playlist.Q != null) ? playlist.Q : "");
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.fillInStackTrace();
} }
}); });
} else { } else {
@@ -11,6 +11,7 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AccountManagerUtils; import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
@@ -51,19 +52,13 @@ public class CatalogJsonInjector {
if (!MusicCacheImpl.isEmpty() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list if (!MusicCacheImpl.isEmpty() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
var noPlaylists = !json.has("playlists"); var noPlaylists = !json.has("playlists");
if (noPlaylists) { PlaylistHelper.addCachedPlaylists(json.optJSONArray("playlists"), noPlaylists);
json.put("playlists", new JSONArray().put(getPlaylist()));
Log.d("catalogInjector", "added new pl");
} else {
json.optJSONArray("playlists").put(getPlaylist());
Log.d("catalogInjector", "added to exist pl");
}
if (!useOldAppVer || noPlaylists) { if (!useOldAppVer || noPlaylists) {
var newBlocks = new JSONArray(); var newBlocks = new JSONArray();
newBlocks newBlocks
.put(getCatalogHeader()) .put(getCatalogHeader("Кешированные плейлисты"))
.put(getCatalogPlaylist()) .put(getCatalogPlaylist())
.put(getCatalogSeparator()); .put(getCatalogSeparator());
@@ -98,23 +93,13 @@ public class CatalogJsonInjector {
var noPlaylists = !json.has("playlists"); var noPlaylists = !json.has("playlists");
if (noPlaylists) { PlaylistHelper.addCachedPlaylists(json.optJSONArray("playlists"), noPlaylists);
json.put("playlists", new JSONArray().put(getPlaylist()));
Log.d("catalogInjector", "added new pl");
} else {
try {
json.optJSONArray("playlists").put(getPlaylist());
Log.d("catalogInjector", "added to exist pl");
} catch (Exception e) {
Log.d("catalogInjector", e.toString());
}
}
if (!useOldAppVer && !noPlaylists) { if (!useOldAppVer && !noPlaylists) {
var newBlocks = new JSONArray(); var newBlocks = new JSONArray();
newBlocks newBlocks
.put(getCatalogHeader()) .put(getCatalogHeader("Кешированные плейлисты"))
.put(getCatalogPlaylist()) .put(getCatalogPlaylist())
.put(getCatalogSeparator()); .put(getCatalogSeparator());
@@ -132,12 +117,15 @@ public class CatalogJsonInjector {
if (type.equals("music_playlists") && j.has("playlists_ids")) { if (type.equals("music_playlists") && j.has("playlists_ids")) {
var newarr = new JSONArray(); var newarr = new JSONArray();
var playlists_ids = j.optJSONArray("playlists_ids"); var playlists_ids = j.getJSONArray("playlists_ids");
var savedPlaylists = PlaylistHelper.getCachedPlaylistsIds();
newarr.put(getUserId() + "_-1"); for (int n = 0; n < savedPlaylists.length(); n++) {
newarr.put(savedPlaylists.getString(n));
}
for (int n = 0; n < playlists_ids.length(); n++) { for (int n = 0; n < playlists_ids.length(); n++) {
newarr.put(playlists_ids.optString(n)); newarr.put(playlists_ids.getString(n));
} }
Log.d("catalogInjector", "added to pl ids"); Log.d("catalogInjector", "added to pl ids");
@@ -220,27 +208,33 @@ public class CatalogJsonInjector {
} }
private static void removeUnsupportedLayouts(JSONArray blocks) throws JSONException { private static void removeUnsupportedLayouts(JSONArray blocks) throws JSONException {
for (int i = 0; i < blocks.length(); i++) { for (int i = blocks.length() - 1; i >= 0; i--) {
var block = blocks.optJSONObject(i); JSONObject block = blocks.optJSONObject(i);
var layout = block.optJSONObject("layout"); JSONObject layout = block.optJSONObject("layout");
var data_type = block.optString("data_type"); String data_type = block.optString("data_type");
if (Objects.equals(data_type, "music_recommended_playlists")) { if (Objects.equals(data_type, "music_recommended_playlists") || Objects.equals(data_type, "radiostations")) {
blocks.remove(i); // block
if (i - 1 >= 0) blocks.remove(i - 1); // text
if (i - 2 >= 0) blocks.remove(i - 2); // divider
continue; // Skip to the next iteration after removing
}
if (layout == null) continue; // Skip to the next iteration if layout is null
String name = layout.optString("name");
if (!name.equals("header_extended")) {
continue; // Skip to the next iteration if not header_extended
}
if (layout.has("top_title")) {
blocks.remove(i); blocks.remove(i);
blocks.remove(i - 1);
blocks.remove(i - 2);
}
if (layout != null) {
var name = layout.optString("name");
if (name.equals("header_extended")) {
if (layout.has("top_title")) blocks.remove(i);
layout.put("name", "header");
}
} }
layout.put("name", "header");
} }
} }
private static void setDefaultAudioPage(JSONArray jsonArray, JSONObject catalog) throws JSONException { private static void setDefaultAudioPage(JSONArray jsonArray, JSONObject catalog) throws JSONException {
for (int i = 0; i < jsonArray.length(); i++) { for (int i = 0; i < jsonArray.length(); i++) {
var item = jsonArray.optJSONObject(i); var item = jsonArray.optJSONObject(i);
@@ -273,7 +267,9 @@ public class CatalogJsonInjector {
+ "&access_token=" + "&access_token="
+ AccountManagerUtils.getUserToken()) + AccountManagerUtils.getUserToken())
.a("Accept-Encoding", "gzip") .a("Accept-Encoding", "gzip")
.a(Headers.a("User-Agent", Network.l.c().a(), "Content-Type", "application/x-www-form-urlencoded; charset=utf-8")).a(); .a("User-Agent", Network.l.c().a())
.a("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
.a();
try (Response resp = Network.b(CLIENT_API).a(request).execute()) { try (Response resp = Network.b(CLIENT_API).a(request).execute()) {
var json = new JSONObject(GzipDecompressor.decompressResponse(resp)).getJSONObject("response"); var json = new JSONObject(GzipDecompressor.decompressResponse(resp)).getJSONObject("response");
var catalogarr = json.optJSONObject("catalog").optJSONArray("sections").optJSONObject(0); var catalogarr = json.optJSONObject("catalog").optJSONArray("sections").optJSONObject(0);
@@ -18,8 +18,12 @@ import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PlaylistInjector { public class PlaylistInjector {
private static final ExecutorService executor = Executors.newCachedThreadPool();
public final static String CHANNEL_NAME = "VTCH"; public final static String CHANNEL_NAME = "VTCH";
public static void injectDownloadPlaylist(Playlist playlist) { public static void injectDownloadPlaylist(Playlist playlist) {
@@ -32,7 +36,7 @@ public class PlaylistInjector {
public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) { public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) {
try { try {
if (!eligibleForOfflineCaching()) if (MusicCacheImpl.isEmpty())
return null; return null;
var requestArgs = audioGetPlaylist.b(); var requestArgs = audioGetPlaylist.b();
@@ -42,8 +46,6 @@ public class PlaylistInjector {
boolean isVirtualPlaylist = accessKey != null && (accessKey.equals("cache")); boolean isVirtualPlaylist = accessKey != null && (accessKey.equals("cache"));
boolean isAlbumVirtualPlaylist = accessKey != null && (accessKey.equals("cacheAlbum")); boolean isAlbumVirtualPlaylist = accessKey != null && (accessKey.equals("cacheAlbum"));
Log.d("PlaylistInjector", "id = " + id + " / owner = " + ownerId + " / access = " + accessKey + " / " + isVirtualPlaylist + " + " + isAlbumVirtualPlaylist);
if (TextUtils.isEmpty(id) || (!isVirtualPlaylist && !isAlbumVirtualPlaylist)) if (TextUtils.isEmpty(id) || (!isVirtualPlaylist && !isAlbumVirtualPlaylist))
return null; return null;
@@ -70,7 +72,7 @@ public class PlaylistInjector {
response.c = tracks; response.c = tracks;
} catch (RemoteException | JSONException e) { } catch (RemoteException | JSONException e) {
e.printStackTrace(); e.fillInStackTrace();
response.c = new ArrayList<>(); response.c = new ArrayList<>();
} }
observableEmitter.b(response); observableEmitter.b(response);
@@ -79,18 +81,20 @@ public class PlaylistInjector {
return; return;
} }
if (isVirtualPlaylist) { response.c = (ArrayList<MusicTrack>) MusicCacheImpl.getPlaylistSongs(ownerId, id);
response.c = (ArrayList<MusicTrack>) TracklistHelper.getMyCachedMusicTracks();
Log.d("Playlist", "Open playlist " + ownerId + "_" + id);
if (Objects.equals(id, "-1")) {
response.b = PlaylistHelper.createCachedPlaylistMetadata(); response.b = PlaylistHelper.createCachedPlaylistMetadata();
} else { } else {
response.c = (ArrayList<MusicTrack>) TracklistHelper.getTracks(requestArgs.get("owner_id")); response.b = MusicCacheImpl.getPlaylist(id, ownerId);
response.b = PlaylistHelper.createCachedPlaylistMetadata(requestArgs.get("owner_id"));
} }
observableEmitter.b(response); observableEmitter.b(response);
}); });
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.fillInStackTrace();
} }
return null; return null;
} }
@@ -12,6 +12,7 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper; import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
import ru.vtosters.lite.music.cache.helpers.TracklistHelper; import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
import ru.vtosters.lite.utils.AccountManagerUtils; import ru.vtosters.lite.utils.AccountManagerUtils;
@@ -22,6 +23,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static ru.vtosters.lite.music.cache.helpers.PlaylistHelper.*;
public class TracklistInjector { public class TracklistInjector {
public static boolean eligibleForOfflineCaching() { public static boolean eligibleForOfflineCaching() {
@@ -59,7 +62,9 @@ public class TracklistInjector {
if (MusicCacheImpl.isEmpty()) { if (MusicCacheImpl.isEmpty()) {
observableEmitter.b(parser.c(getEmptyCatalog())); observableEmitter.b(parser.c(getEmptyCatalog()));
} else { } else {
observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks()))); observableEmitter.b(parser.c(createVirtualCatalog(
PlaylistCacheDbDelegate.getTracksInPlaylist(AndroidUtils.getGlobalContext(),
AccountManagerUtils.getUserId() + "_-1"))));
} }
} }
}); });
@@ -162,16 +167,12 @@ public class TracklistInjector {
boolean noPlaylists = hasNoPlaylists(blocks); boolean noPlaylists = hasNoPlaylists(blocks);
if (noPlaylists) { if (noPlaylists) {
catalogNode.put("playlists", new JSONArray().put(PlaylistHelper.getPlaylist())); PlaylistHelper.addCachedPlaylists(catalogNode.optJSONArray("playlists"), true);
JSONArray newBlocks = createNewBlocks(blocks); firstSection.put("blocks", createNewBlocks(blocks));
Log.d("catalogInjector", "added cache catalog playlist");
firstSection.put("blocks", newBlocks);
} else { } else {
try { try {
catalogNode.optJSONArray("playlists").put(PlaylistHelper.getPlaylist()); PlaylistHelper.addCachedPlaylists(catalogNode.optJSONArray("playlists"), false);
updatePlaylistsIds(blocks); updatePlaylistsIds(blocks);
} catch (Exception e) { } catch (Exception e) {
@@ -188,7 +189,7 @@ public class TracklistInjector {
return firstSection.getString("url").equals("https://vk.com/audios" + AccountManagerUtils.getUserId() + "?section=all"); return firstSection.getString("url").equals("https://vk.com/audios" + AccountManagerUtils.getUserId() + "?section=all");
} }
private static boolean hasNoPlaylists(JSONArray blocks) throws JSONException { private static boolean hasNoPlaylists(JSONArray blocks) {
return blocks.optJSONObject(0).optJSONArray("buttons").optJSONObject(0).optJSONObject("action").optString("type").equals("create_playlist"); return blocks.optJSONObject(0).optJSONArray("buttons").optJSONObject(0).optJSONObject("action").optString("type").equals("create_playlist");
} }
@@ -196,13 +197,14 @@ public class TracklistInjector {
JSONArray newBlocks = new JSONArray(); JSONArray newBlocks = new JSONArray();
newBlocks newBlocks
.put(PlaylistHelper.getCatalogHeader()) .put(PlaylistHelper.getCatalogHeader(AndroidUtils.getString(R.string.cached_tracks_title)))
.put(PlaylistHelper.getCatalogPlaylist()) .put(PlaylistHelper.getCatalogPlaylist())
.put(PlaylistHelper.getCatalogSeparator()); .put(PlaylistHelper.getCatalogSeparator());
for (int i = 0; i < blocks.length(); i++) { for (int i = 0; i < blocks.length(); i++) {
newBlocks.put(blocks.optJSONObject(i)); newBlocks.put(blocks.optJSONObject(i));
} }
return newBlocks; return newBlocks;
} }
@@ -212,16 +214,15 @@ public class TracklistInjector {
String type = j.optString("data_type"); String type = j.optString("data_type");
if (type.equals("music_playlists") && j.has("playlists_ids")) { if (type.equals("music_playlists") && j.has("playlists_ids")) {
JSONArray newarr = new JSONArray(); JSONArray newarr = PlaylistHelper.getCachedPlaylistsIds();
JSONArray playlists_ids = j.optJSONArray("playlists_ids"); JSONArray playlists_ids = j.optJSONArray("playlists_ids");
newarr.put(AccountManagerUtils.getUserId() + "_-1");
for (int n = 0; n < playlists_ids.length(); n++) { for (int n = 0; n < playlists_ids.length(); n++) {
newarr.put(playlists_ids.optString(n)); newarr.put(playlists_ids.optString(n));
} }
j.put("playlists_ids", newarr); j.put("playlists_ids", newarr);
break; break;
} }
} }
@@ -240,7 +241,19 @@ public class TracklistInjector {
audiosBlock.put("layout", layout); audiosBlock.put("layout", layout);
var blocks = new JSONArray().put(getShuffleButton()).put(audiosBlock); var blocks = new JSONArray();
if (MusicCacheImpl.hasPlaylist()) {
blocks.put(getCatalogHeader("Кешированные плейлисты"))
.put(getCatalogPlaylist());
}
if (!MusicCacheImpl.getAllOwnTracks().isEmpty()) {
if (MusicCacheImpl.hasPlaylist()) blocks.put(getCatalogSeparator());
blocks.put(getCatalogHeader(AndroidUtils.getString(R.string.cached_tracks_title)))
.put(getShuffleButton())
.put(audiosBlock);
}
var randomId = getRandomId(); var randomId = getRandomId();
@@ -259,6 +272,7 @@ public class TracklistInjector {
return new JSONObject() return new JSONObject()
.put("catalog", catalog) .put("catalog", catalog)
.put("playlists", addCachedPlaylists(new JSONArray(), false))
.put("audios", TracklistHelper.tracksToJsons(tracks)); .put("audios", TracklistHelper.tracksToJsons(tracks));
} }
@@ -1,27 +1,35 @@
package ru.vtosters.lite.downloaders; package ru.vtosters.lite.downloaders;
import android.os.Environment; import android.os.Environment;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.core.util.ToastUtils; import com.vk.core.util.ToastUtils;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist; import com.vk.dto.music.Playlist;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import java8.util.concurrent.CompletableFuture; import org.json.JSONObject;
import ru.vtosters.hooks.music.MusicCacheFilesHook; import ru.vtosters.hooks.music.MusicCacheFilesHook;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
import ru.vtosters.lite.music.callback.MusicCallbackBuilder; import ru.vtosters.lite.music.callback.MusicCallbackBuilder;
import ru.vtosters.lite.music.converter.playlist.PlaylistConverter; import ru.vtosters.lite.music.converter.playlist.PlaylistConverter;
import ru.vtosters.lite.music.downloader.AudioGet; import ru.vtosters.lite.music.downloader.AudioGet;
import ru.vtosters.lite.music.downloader.PlaylistDownloader; import ru.vtosters.lite.music.downloader.PlaylistDownloader;
import ru.vtosters.lite.music.downloader.ThumbnailPlaylistDownloader;
import ru.vtosters.lite.music.downloader.TrackDownloader; import ru.vtosters.lite.music.downloader.TrackDownloader;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.notification.MusicNotificationBuilder; import ru.vtosters.lite.music.notification.MusicNotificationBuilder;
import ru.vtosters.lite.utils.AccountManagerUtils; import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils; import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.PlaylistUtils;
import java.io.File; import java.io.File;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import static ru.vtosters.lite.utils.AndroidUtils.getString; import static ru.vtosters.lite.utils.AndroidUtils.getString;
@@ -35,18 +43,18 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
public class AudioDownloader { public class AudioDownloader {
public static final String dlpath = Preferences.getBoolValue("dldir", false) ? Environment.DIRECTORY_DOWNLOADS : Environment.DIRECTORY_MUSIC; public static final String dlpath = Preferences.getBoolValue("dldir", false) ? Environment.DIRECTORY_DOWNLOADS : Environment.DIRECTORY_MUSIC;
public static CompletableFuture<Void> downloadPlaylist(Playlist playlist) { public static void downloadPlaylist(Playlist playlist) {
var tracks = PlaylistConverter.getPlaylist(playlist); List<MusicTrack> tracks = PlaylistConverter.getPlaylist(playlist);
var playlistName = IOUtils.getValidFileName(playlist.g); String playlistName = IOUtils.getValidFileName(playlist.g);
var musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath(); String musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath();
var downloadPath = musicPath + File.separator + playlistName; String downloadPath = musicPath + File.separator + playlistName;
var notificationId = playlistName.hashCode(); int notificationId = playlistName.hashCode();
var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlistName, notificationId); NotificationCompat.Builder notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlistName, notificationId);
return PlaylistDownloader.downloadPlaylist( PlaylistDownloader.downloadPlaylist(
tracks, tracks,
IOUtils.getValidFileName(playlist.g), IOUtils.getValidFileName(playlist.g),
downloadPath, downloadPath,
@@ -60,53 +68,96 @@ public class AudioDownloader {
} }
public static void cacheTrack(MusicTrack track) { public static void cacheTrack(MusicTrack track) {
var trackId = LibVKXClient.asId(track); String trackId = LibVKXClient.asId(track);
if (MusicCacheImpl.isCachedTrack(trackId)) { if (MusicCacheImpl.isCachedTrack(trackId)) {
MusicCacheImpl.removeTrack(trackId); MusicCacheImpl.removeTrack(trackId);
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache")); AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
} else { } else {
var trackFile = MusicCacheFilesHook.getTrackFile(trackId); File trackFile = MusicCacheFilesHook.getTrackFile(trackId);
if (!trackFile.exists()) if (!trackFile.exists()) {
trackFile.getParentFile().mkdirs(); trackFile.getParentFile().mkdirs();
}
downloadM3U8(track, true); downloadM3U8(track, true);
} }
} }
public static CompletableFuture<Void> cachePlaylist(Playlist playlist) { public static void cachePlaylist(Playlist playlist) {
var tracks = PlaylistConverter.getPlaylist(playlist); List<MusicTrack> tracks = PlaylistConverter.getPlaylist(playlist);
var notificationId = playlist.g.hashCode(); int notificationId = playlist.g.hashCode();
var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlist.g, notificationId); NotificationCompat.Builder notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlist.g, notificationId);
return PlaylistDownloader.cachePlaylist( JSONObject thumbs = PlaylistUtils.getThumb(playlist);
if (thumbs != null) {
new ThumbnailPlaylistDownloader().download(null, new Callback() {
@Override
public void onProgress(int progress) {
}
@Override
public void onSuccess() {
PlaylistCacheDbDelegate.addPlaylist(AndroidUtils.getGlobalContext(), playlist);
Log.d("Playlist", "adding to cache with thumbs " + playlist.v1());
}
@Override
public void onFailure(Throwable e) {
throw new RuntimeException(e);
}
@Override
public void onSizeReceived(long size, long header) {
}
}, playlist);
} else {
PlaylistCacheDbDelegate.addPlaylist(AndroidUtils.getGlobalContext(), playlist);
Log.d("Playlist", "adding to cache without thumbs " + playlist.a);
}
PlaylistDownloader.cachePlaylist(
tracks, tracks,
MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId) MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId),
playlist
); );
} }
public static void cacheAllAudios() { public static void cacheAllAudios() {
var tracks = AudioGet.getAudios(); List<MusicTrack> tracks = AudioGet.getAudios();
var notificationId = AccountManagerUtils.getUserId(); int notificationId = AccountManagerUtils.getUserId();
var notification = MusicNotificationBuilder.buildAllAudiosDownloadNotification(notificationId); NotificationCompat.Builder notification = MusicNotificationBuilder.buildAllAudiosDownloadNotification(notificationId);
if (tracks == null || tracks.isEmpty()) {
return;
}
PlaylistDownloader.cachePlaylist( PlaylistDownloader.cachePlaylist(
tracks, tracks,
MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId) MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId),
PlaylistHelper.createCachedPlaylistMetadata()
); );
} }
public static void downloadAllAudios() { public static void downloadAllAudios() {
var tracks = AudioGet.getAudios(); List<MusicTrack> tracks = AudioGet.getAudios();
var notificationId = AccountManagerUtils.getUserId(); int notificationId = AccountManagerUtils.getUserId();
var notification = MusicNotificationBuilder.buildAllAudiosDownloadNotification(notificationId); NotificationCompat.Builder notification = MusicNotificationBuilder.buildAllAudiosDownloadNotification(notificationId);
var playlistName = "Audios of " + AccountManagerUtils.getUserId(); String playlistName = "Audios of " + AccountManagerUtils.getUserId();
var musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath(); String musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath();
var downloadPath = musicPath + File.separator + playlistName; String downloadPath = musicPath + File.separator + playlistName;
if (tracks == null || tracks.isEmpty()) {
return;
}
PlaylistDownloader.downloadPlaylist( PlaylistDownloader.downloadPlaylist(
tracks, tracks,
@@ -116,21 +167,21 @@ public class AudioDownloader {
); );
} }
private static void private static void downloadM3U8(MusicTrack track, boolean cache) {
downloadM3U8(MusicTrack track, boolean cache) {
Objects.requireNonNull(track.D, () -> { Objects.requireNonNull(track.D, () -> {
ToastUtils.a(getString(R.string.link_audio_error)); ToastUtils.a(getString(R.string.link_audio_error));
return "link must not be null"; return "link must not be null";
}); });
var musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath(); String musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath();
var tempId = track.d; int tempId = track.d;
var downloadPath = musicPath + File.separator; String downloadPath = musicPath + File.separator;
var notification = MusicNotificationBuilder.buildDownloadNotification(track, tempId); NotificationCompat.Builder notification = MusicNotificationBuilder.buildDownloadNotification(track, tempId);
var cb = MusicCallbackBuilder.buildOneTrackCallback(tempId, notification); Callback cb = MusicCallbackBuilder.buildOneTrackCallback(tempId, notification);
if (cache) if (cache) {
TrackDownloader.cacheTrack(track, cb); TrackDownloader.cacheTrack(track, cb, PlaylistHelper.createCachedPlaylistMetadata());
else } else {
TrackDownloader.downloadTrack(track, downloadPath, cb); TrackDownloader.downloadTrack(track, downloadPath, cb);
}
} }
} }
@@ -10,6 +10,10 @@ import java.util.List;
import bruhcollective.itaysonlab.libvkx.ILibVkxService; import bruhcollective.itaysonlab.libvkx.ILibVkxService;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl; import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
import com.vk.dto.music.Playlist;
import ru.vtosters.lite.music.cache.delegate.MusicCacheDbDelegate;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils; import ru.vtosters.lite.utils.music.MusicTrackUtils;
@@ -34,20 +38,28 @@ public class MusicCacheImpl {
MusicCacheStorageUtils.removeTrackDirById(trackId); MusicCacheStorageUtils.removeTrackDirById(trackId);
} }
public static List<MusicTrack> getAllTracks() { public static List<MusicTrack> getAllOwnTracks() {
return MusicCacheDbDelegate.getAllTracks(AndroidUtils.getGlobalContext()); return PlaylistCacheDbDelegate.getTracksInPlaylist(AndroidUtils.getGlobalContext(), AccountManagerUtils.getUserId() + "_-1");
} }
public static List<MusicTrack> getAlbumById(String albumId) { public static List<MusicTrack> getPlaylistSongs(String owner_id, String playlist_id) {
return MusicCacheDbDelegate.getAlbumById(AndroidUtils.getGlobalContext(), albumId); return PlaylistCacheDbDelegate.getTracksInPlaylist(AndroidUtils.getGlobalContext(), owner_id + "_" + playlist_id); // get songs from playlist
} }
public static List<MusicTrack> getFirstAlbumTrack(String albumId) { public static Playlist getPlaylist(String playlist_id, String owner_id) {
return MusicCacheDbDelegate.getFirstAlbumTrack(AndroidUtils.getGlobalContext(), albumId); return getPlaylistById(owner_id + "_" + playlist_id); // get playlist from db
} }
public static List<MusicTrack> getPlaylist() { public static List<Playlist> getPlaylists() {
return MusicCacheDbDelegate.getPlaylist(AndroidUtils.getGlobalContext()); return PlaylistCacheDbDelegate.getAllPlaylists(AndroidUtils.getGlobalContext()); // get all cached playlists
}
public static Playlist getPlaylistById(String query) {
return PlaylistCacheDbDelegate.getPlaylistById(AndroidUtils.getGlobalContext(), query); // get playlist from db
}
public static boolean hasPlaylist() {
return PlaylistCacheDbDelegate.getPlaylistsCount(AndroidUtils.getGlobalContext()) != 0; // has playlists or not
} }
public static long getTracksCount() { public static long getTracksCount() {
@@ -60,7 +72,7 @@ public class MusicCacheImpl {
try { try {
return (long) service.getCache().size(); return (long) service.getCache().size();
} catch (RemoteException e) { } catch (RemoteException e) {
e.printStackTrace(); e.fillInStackTrace();
return defaultValue(); return defaultValue();
} }
} }
@@ -82,6 +94,7 @@ public class MusicCacheImpl {
public static void clear() { public static void clear() {
MusicCacheDbDelegate.drop(AndroidUtils.getGlobalContext()); MusicCacheDbDelegate.drop(AndroidUtils.getGlobalContext());
PlaylistCacheDbDelegate.drop(AndroidUtils.getGlobalContext());
MusicCacheStorageUtils.clear(); MusicCacheStorageUtils.clear();
} }
} }
@@ -24,6 +24,8 @@ import java.net.URLDecoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils; import ru.vtosters.lite.utils.music.MusicTrackUtils;
@@ -127,6 +129,8 @@ public class MusicCacheDb extends SQLiteOpenHelper implements AutoCloseable { //
vals.put(Constants.COLUMN_DURATION, track.h); vals.put(Constants.COLUMN_DURATION, track.h);
vals.put(Constants.COLUMN_HAS_ARTWORK, track.I != null && track.I.u1() != null); vals.put(Constants.COLUMN_HAS_ARTWORK, track.I != null && track.I.u1() != null);
db.insert(Constants.TABLE_NAME, null, vals); db.insert(Constants.TABLE_NAME, null, vals);
PlaylistCacheDbDelegate.addTrackToPlaylist(AndroidUtils.getGlobalContext(), AccountManagerUtils.getUserId() + "_-1", track.y1());
} }
} }
} }
@@ -170,18 +174,21 @@ public class MusicCacheDb extends SQLiteOpenHelper implements AutoCloseable { //
return DatabaseUtils.queryNumEntries(getReadableDatabase(), Constants.TABLE_NAME); return DatabaseUtils.queryNumEntries(getReadableDatabase(), Constants.TABLE_NAME);
} }
public MusicTrack getTrackById(String trackId) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(Constants.TABLE_NAME, null, Constants.COLUMN_TRACK_ID + " = ?", new String[]{trackId}, null, null, null);
MusicTrack track = null;
if (cursor.moveToFirst()) {
track = fromCursor(cursor);
}
cursor.close();
return track;
}
public List<MusicTrack> getAllTracks() { public List<MusicTrack> getAllTracks() {
return getTracksWithCursor(getReadableDatabase().query(Constants.TABLE_NAME, null, null, null, null, null, null)); return getTracksWithCursor(getReadableDatabase().query(Constants.TABLE_NAME, null, null, null, null, null, null));
} }
public List<MusicTrack> getAlbum(String albumId) {
return getAlbumTracks(albumId, null);
}
public List<MusicTrack> getFirstAlbumTrack(String albumId) {
return getAlbumTracks(albumId, "0,1");
}
private List<MusicTrack> getAlbumTracks(String albumId, @Nullable String limit) { private List<MusicTrack> getAlbumTracks(String albumId, @Nullable String limit) {
return getTracksWithCursor(getReadableDatabase().query( return getTracksWithCursor(getReadableDatabase().query(
Constants.TABLE_NAME, Constants.TABLE_NAME,
@@ -220,7 +227,7 @@ public class MusicCacheDb extends SQLiteOpenHelper implements AutoCloseable { //
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface Constants { public @interface Constants {
int DV_VERSION = 0x2; int DV_VERSION = 0x3;
String DB_NAME = "vt_lite_cache.db"; String DB_NAME = "vt_lite_cache.db";
String TABLE_NAME = "tracks"; String TABLE_NAME = "tracks";
//columns //columns
@@ -0,0 +1,254 @@
package ru.vtosters.lite.music.cache.db;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import static ru.vtosters.lite.utils.music.MusicCacheStorageUtils.*;
public class PlaylistCacheDb extends SQLiteOpenHelper implements AutoCloseable {
public PlaylistCacheDb(Context context) {
super(context, Constants.DB_NAME, null, Constants.DV_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(Constants.CREATE_QUERY);
db.execSQL(Constants.CREATE_PLAYLIST_TRACKS_QUERY);
}
private static Playlist fromCursor(Cursor cur) throws JSONException {
@SuppressLint("Range")
String photoString = cur.getString(cur.getColumnIndex(Constants.COLUMN_PHOTO));
JSONObject photo = null;
try {
photo = new JSONObject(photoString);
} catch (JSONException e) {
// ok and
}
@SuppressLint("Range")
JSONObject playlist = PlaylistHelper.generatePlaylist(
cur.getInt(cur.getColumnIndex(Constants.COLUMN_ID)),
cur.getInt(cur.getColumnIndex(Constants.COLUMN_OWNER_ID)),
Boolean.parseBoolean(cur.getString(cur.getColumnIndex(Constants.COLUMN_IS_EXPLICIT))),
cur.getString(cur.getColumnIndex(Constants.COLUMN_TITLE)),
cur.getString(cur.getColumnIndex(Constants.COLUMN_DESCRIPTION)),
photo,
0);
Log.d("Playlist", "generated " + Playlist.U.a(playlist).v1());
return Playlist.U.a(playlist);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(Constants.DROP_QUERY);
onCreate(db);
}
public void addPlaylist(int id, int ownerId, boolean isExplicit, String title, String description, JSONObject photo) {
ContentValues values = new ContentValues();
values.put(Constants.COLUMN_ID, id);
values.put(Constants.COLUMN_OWNER_ID, ownerId);
values.put(Constants.COLUMN_IS_EXPLICIT, isExplicit);
values.put(Constants.COLUMN_TITLE, title);
values.put(Constants.COLUMN_DESCRIPTION, description);
values.put(Constants.COLUMN_PHOTO, String.valueOf(photo));
getWritableDatabase().insert(Constants.TABLE_NAME, null, values);
}
private void deletePlaylist(int id, int ownerId) {
getWritableDatabase().delete(Constants.TABLE_NAME, Constants.COLUMN_ID + " = ? AND " + Constants.COLUMN_OWNER_ID + " = ?", new String[]{String.valueOf(id), String.valueOf(ownerId)});
}
public boolean isCachedPlaylist(int id, int ownerId) {
Cursor cursor = getReadableDatabase().query(Constants.TABLE_NAME, new String[]{Constants.COLUMN_ID}, Constants.COLUMN_ID + " = ? AND " + Constants.COLUMN_OWNER_ID + " = ?", new String[]{String.valueOf(id), String.valueOf(ownerId)}, null, null, null);
boolean exists = cursor.getCount() > 0;
cursor.close();
return exists;
}
public long getPlaylistsCount() {
return DatabaseUtils.queryNumEntries(getReadableDatabase(), Constants.TABLE_NAME);
}
public void deletePlaylistWithTracks(String playlistId) {
SQLiteDatabase db = getWritableDatabase();
String[] parts = playlistId.split("_");
int id = Integer.parseInt(parts[1]);
int ownerId = Integer.parseInt(parts[0]);
// Get all tracks in the playlist
List<MusicTrack> tracks = getTracksInPlaylist(playlistId);
// Delete the playlist
deletePlaylist(id, ownerId);
// Delete each track from the playlist_tracks table
for (MusicTrack track : tracks) {
String trackId = track.y1();
removeTrackFromPlaylist(playlistId, trackId);
removePlaylistThumb(playlistId);
if (!isTrackInAnyOtherPlaylist(trackId, playlistId)) {
MusicCacheImpl.removeTrack(trackId);
Log.d("Playlist", "removed " + trackId);
}
}
// Delete all entries from the playlist_tracks table for this playlist
db.delete(Constants.TABLE_PLAYLIST_TRACKS, Constants.COLUMN_PLAYLIST_ID + " = ?", new String[]{playlistId});
}
private boolean isTrackInAnyOtherPlaylist(String trackId, String playlistId) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(
Constants.TABLE_PLAYLIST_TRACKS,
null,
Constants.COLUMN_TRACK_ID + " = ? AND " + Constants.COLUMN_PLAYLIST_ID + " != ?",
new String[]{trackId, playlistId},
null, null, null
);
boolean exists = cursor.getCount() > 0;
cursor.close();
return exists;
}
public List<Playlist> getAllPlaylists() {
Cursor cursor = getReadableDatabase().query(Constants.TABLE_NAME, null, null, null, null, null, null);
List<Playlist> playlists = getPlaylistsWithCursor(cursor);
cursor.close();
return playlists;
}
public Playlist getPlaylistById(int id, int ownerId) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(Constants.TABLE_NAME, null, Constants.COLUMN_ID + " = ? AND " + Constants.COLUMN_OWNER_ID + " = ?", new String[]{String.valueOf(id), String.valueOf(ownerId)}, null, null, null);
Playlist playlist = null;
if (cursor.moveToFirst()) {
try {
playlist = fromCursor(cursor);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
cursor.close();
return playlist;
}
private List<Playlist> getPlaylistsWithCursor(Cursor cur) {
List<Playlist> playlists = new ArrayList<>();
while (cur.moveToNext()) {
try {
playlists.add(fromCursor(cur));
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
return playlists;
}
public void addTrackToPlaylist(String playlistId, String trackId) {
ContentValues values = new ContentValues();
values.put(Constants.COLUMN_PLAYLIST_ID, playlistId);
values.put(Constants.COLUMN_TRACK_ID, trackId);
getWritableDatabase().insert(Constants.TABLE_PLAYLIST_TRACKS, null, values);
}
public void removeTrackFromPlaylist(String playlistId, String trackId) {
getWritableDatabase().delete(Constants.TABLE_PLAYLIST_TRACKS, Constants.COLUMN_PLAYLIST_ID + " = ? AND " + Constants.COLUMN_TRACK_ID + " = ?", new String[]{playlistId, trackId});
}
public List<MusicTrack> getTracksInPlaylist(String playlistId) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(
Constants.TABLE_PLAYLIST_TRACKS,
new String[]{Constants.COLUMN_TRACK_ID},
Constants.COLUMN_PLAYLIST_ID + " = ?",
new String[]{playlistId},
null, null, null
);
List<MusicTrack> tracks = new ArrayList<>();
while (cursor.moveToNext()) {
@SuppressLint("Range")
String trackId = cursor.getString(cursor.getColumnIndex(Constants.COLUMN_TRACK_ID));
MusicTrack track = TracklistHelper.getTrack(trackId);
Log.d("Playlist", "add " + trackId + " to playlist " + playlistId);
if (track != null) {
tracks.add(track);
}
}
cursor.close();
return tracks;
}
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
db.setForeignKeyConstraintsEnabled(true);
}
@Retention(RetentionPolicy.SOURCE)
public @interface Constants {
int DV_VERSION = 0x2;
String DB_NAME = "vt_lite_cache_playlists.db";
String TABLE_NAME = "playlists";
//columns
String COLUMN_ID = "id";
String COLUMN_OWNER_ID = "owner_id";
String COLUMN_IS_EXPLICIT = "is_explicit";
String COLUMN_TITLE = "title";
String COLUMN_DESCRIPTION = "description";
String COLUMN_PHOTO = "photo";
// tracks
String COLUMN_TRACK_ID = "track_id";
String TABLE_PLAYLIST_TRACKS = "playlist_tracks";
String COLUMN_PLAYLIST_ID = "playlist_id";
//queries
String CREATE_QUERY = "create table " + TABLE_NAME
+ "("
+ COLUMN_ID + " INTEGER PRIMARY KEY autoincrement,"
+ COLUMN_OWNER_ID + " INTEGER not null,"
+ COLUMN_IS_EXPLICIT + " TEXT not null,"
+ COLUMN_TITLE + " TEXT not null,"
+ COLUMN_DESCRIPTION + " TEXT not null,"
+ COLUMN_PHOTO + " TEXT not null"
+ ")";
String CREATE_PLAYLIST_TRACKS_QUERY = "create table " + TABLE_PLAYLIST_TRACKS + "("
+ COLUMN_PLAYLIST_ID + " TEXT not null,"
+ COLUMN_TRACK_ID + " TEXT not null"
+ ")";
String DROP_QUERY = "drop table if exists " + TABLE_NAME;
}
}
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.cache; package ru.vtosters.lite.music.cache.delegate;
import android.content.Context; import android.content.Context;
@@ -43,27 +43,9 @@ public class MusicCacheDbDelegate {
} }
} }
public static List<MusicTrack> getAllTracks(Context context) { public static MusicTrack getTrackById(Context context, String trackId) {
try (var db = connectToDb(context)) { try (var db = connectToDb(context)) {
return db.getAllTracks(); return db.getTrackById(trackId);
}
}
public static List<MusicTrack> getAlbumById(Context context, String albumId) {
try (var db = connectToDb(context)) {
return db.getAlbum(albumId);
}
}
public static List<MusicTrack> getFirstAlbumTrack(Context context, String albumId) {
try (var db = connectToDb(context)) {
return db.getFirstAlbumTrack(albumId);
}
}
public static List<MusicTrack> getPlaylist(Context context) {
try (var db = connectToDb(context)) {
return db.getPlaylist();
} }
} }
@@ -0,0 +1,118 @@
package ru.vtosters.lite.music.cache.delegate;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.cache.db.PlaylistCacheDb;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.PlaylistUtils;
import java.util.List;
public class PlaylistCacheDbDelegate {
static synchronized private PlaylistCacheDb connectToDb(Context context) {
synchronized (PlaylistCacheDb.class) {
return new PlaylistCacheDb(context);
}
}
public static void addPlaylist(Context context, Playlist playlist) {
try (var db = connectToDb(context)) {
db.addPlaylist(playlist.a, playlist.b, playlist.C, playlist.g, playlist.B, generatePhotoJSON(playlist));
}
}
public static JSONObject generatePhotoJSON(Playlist playlist) {
if (PlaylistUtils.getThumb(playlist) != null) {
JSONObject photo = new JSONObject();
String playlistId = playlist.v1();
try {
photo.put("height", 300);
photo.put("width", 300);
addPhotoSizeToJSON(photo, playlistId, 600, "photo_600");
addPhotoSizeToJSON(photo, playlistId, 34, "photo_34");
addPhotoSizeToJSON(photo, playlistId, 1200, "photo_1200");
addPhotoSizeToJSON(photo, playlistId, 68, "photo_68");
addPhotoSizeToJSON(photo, playlistId, 135, "photo_135");
addPhotoSizeToJSON(photo, playlistId, 300, "photo_300");
addPhotoSizeToJSON(photo, playlistId, 270, "photo_270");
return photo;
} catch (JSONException e) {
throw new RuntimeException(e);
}
} else {
return null;
}
}
private static void addPhotoSizeToJSON(JSONObject photo, String playlistId, int size, String key) throws JSONException {
photo.put(key, Uri.fromFile(MusicCacheStorageUtils.getPlaylistThumb(playlistId, size)).toString());
Log.d("Playlist", "thumb link " + Uri.fromFile(MusicCacheStorageUtils.getPlaylistThumb(playlistId, size)).toString());
}
public static void deletePlaylist(Context context, String playlistId) {
try (var db = connectToDb(context)) {
db.deletePlaylistWithTracks(playlistId);
}
}
public static boolean isCachedPlaylist(Context context, String id) {
try (var db = connectToDb(context)) {
String[] parts = id.split("_");
int playlistId = Integer.parseInt(parts[1]);
int ownerId = Integer.parseInt(parts[0]);
return db.isCachedPlaylist(playlistId, ownerId);
}
}
public static long getPlaylistsCount(Context context) {
try (var db = connectToDb(context)) {
return db.getPlaylistsCount();
}
}
public static List<Playlist> getAllPlaylists(Context context) {
try (var db = connectToDb(context)) {
return db.getAllPlaylists();
}
}
public static Playlist getPlaylistById(Context context, String playlistId) {
try (var db = connectToDb(context)) {
String[] parts = playlistId.split("_");
int id = Integer.parseInt(parts[1]);
int ownerId = Integer.parseInt(parts[0]);
return db.getPlaylistById(id, ownerId);
}
}
public static void addTrackToPlaylist(Context context, String playlistId, String trackId) {
try (var db = connectToDb(context)) {
db.addTrackToPlaylist(playlistId, trackId);
}
}
public static void removeTrackFromPlaylist(Context context, String playlistId, String trackId) {
try (var db = connectToDb(context)) {
db.removeTrackFromPlaylist(playlistId, trackId);
}
}
public static List<MusicTrack> getTracksInPlaylist(Context context, String playlistId) {
try (var db = connectToDb(context)) {
return db.getTracksInPlaylist(playlistId);
}
}
public static void drop(Context context) {
context.deleteDatabase(PlaylistCacheDb.Constants.DB_NAME);
}
}
@@ -1,96 +1,82 @@
package ru.vtosters.lite.music.cache.helpers; package ru.vtosters.lite.music.cache.helpers;
import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId; import android.util.Log;
import android.net.Uri;
import com.vk.dto.music.Playlist; import com.vk.dto.music.Playlist;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils; import ru.vtosters.lite.utils.NetworkUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils; import ru.vtosters.lite.utils.music.PlaylistUtils;
public class PlaylistHelper { public class PlaylistHelper {
public static Playlist createCachedPlaylistMetadata() { public static Playlist createCachedPlaylistMetadata() {
try { try {
return new Playlist(getPlaylist()); return new Playlist(getCachedSongsPlaylist());
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace();
return null; return null;
} }
} }
public static Playlist createCachedPlaylistMetadata(String id) { public static JSONArray addCachedPlaylists(JSONArray jsonArray, Boolean noPlaylists) {
var track = TracklistHelper.getTracksOne(id).get(0);
var albumLink = track.I;
var thumb = albumLink.u1();
return createAlbum(
thumb != null ? thumb.h(600) : "",
albumLink.getTitle(),
MusicTrackUtils.getArtists(track),
track.y1(),
albumLink.getId()
);
}
public static Playlist createAlbum(String photo, String title, String artist, String trackId, int albumId) {
try { try {
var jPhoto = new JSONObject(); if (noPlaylists) {
if (!photo.isEmpty()) jsonArray.put(new JSONArray().put(getCachedSongsPlaylist()));
jPhoto.put("height", 600) } else {
.put("width", 600) jsonArray.put(getCachedSongsPlaylist());
.putOpt("photo_300", Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId, 300)).toString()) }
.putOpt("photo_600", Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId, 600)).toString());
var obj = new JSONObject() for (Playlist playlist : MusicCacheImpl.getPlaylists()) {
.put("id", -2) if (noPlaylists) {
.put("owner_id", albumId) jsonArray.put(new JSONArray().put(generatePlaylist(playlist.a, playlist.b, playlist.C, playlist.g, playlist.B, PlaylistUtils.getThumb(playlist), playlist.O)));
.put("type", 1) } else {
.put("album_type", "main_only") jsonArray.put(generatePlaylist(playlist.a, playlist.b, playlist.C, playlist.g, playlist.B, PlaylistUtils.getThumb(playlist), playlist.O));
.put("title", title) }
.put("description", albumId + "/ " + AndroidUtils.getString(R.string.cached_album_warning))
.put("main_artists", new JSONArray() Log.d("PlaylistHelper", "Playlist cache added: " + playlist.a + " " + playlist.b + " " + playlist.C + " " + playlist.g + " " + playlist.B);
.put(new JSONObject().put("name", artist) }
.put("id", "-1")
.put("domain", "-1")))
.putOpt("photo", jPhoto)
.put("count", 0);
return new Playlist(obj);
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); e.fillInStackTrace();
return null;
} }
return jsonArray;
} }
public static List<Playlist> getAlbumPlaylists() { public static JSONObject generatePlaylist(int id, int owner_id, boolean is_explicit, String title, String description, JSONObject photo, int count) throws JSONException {
List<Playlist> list = new ArrayList<>(); JSONObject playlist = new JSONObject()
for (var track : MusicCacheImpl.getPlaylist()) { .put("id", id)
var albumLink = track.I; .put("owner_id", owner_id)
var thumb = albumLink.u1(); .put("type", 0)
list.add(createAlbum( .put("album_type", "playlist")
thumb != null ? thumb.h(600) : "", .put("is_explicit", is_explicit)
albumLink.getTitle(), .put("title", title)
MusicTrackUtils.getArtists(track), .put("description", description)
track.y1(), .put("access_key", "cache")
albumLink.getId() .put("genres", new JSONArray())
)); .put("is_following", false)
.put("followers", 0)
.put("plays", 0)
.put("create_time", 0)
.put("update_time", 0)
.put("subtitle", "")
.put("meta", new JSONObject()
.put("view", "compact"))
.put("count", count);
if (photo != null) {
playlist.put("photo", photo);
} }
return list;
return playlist;
} }
public static JSONObject getPlaylist() throws JSONException { public static JSONObject getCachedSongsPlaylist() throws JSONException {
return new JSONObject() return new JSONObject()
.put("id", -1) .put("id", -1)
.put("owner_id", getUserId()) .put("owner_id", AccountManagerUtils.getUserId())
.put("type", 0) .put("type", 0)
.put("album_type", "playlist") .put("album_type", "playlist")
.put("title", AndroidUtils.getString(R.string.cached_tracks_title)) .put("title", AndroidUtils.getString(R.string.cached_tracks_title))
@@ -115,7 +101,21 @@ public class PlaylistHelper {
.put("height", 600)) .put("height", 600))
.put("meta", new JSONObject() .put("meta", new JSONObject()
.put("view", "compact")) .put("view", "compact"))
.put("count", 0); .put("count", MusicCacheImpl.getAllOwnTracks().size());
}
public static JSONArray getCachedPlaylistsIds() {
JSONArray arr = new JSONArray();
if (NetworkUtils.isNetworkConnected() && !MusicCacheImpl.getAllOwnTracks().isEmpty()) {
arr.put(AccountManagerUtils.getUserId() + "_-1");
}
for (var playlist : MusicCacheImpl.getPlaylists()) {
arr.put(playlist.v1());
}
return arr;
} }
public static JSONObject getCatalogPlaylist() throws JSONException { public static JSONObject getCatalogPlaylist() throws JSONException {
@@ -125,18 +125,17 @@ public class PlaylistHelper {
.put("layout", new JSONObject() .put("layout", new JSONObject()
.put("name", "large_slider") .put("name", "large_slider")
.put("is_editable", 0) .put("is_editable", 0)
.put("owner_id", getUserId())) .put("owner_id", AccountManagerUtils.getUserId()))
.put("playlists_ids", new JSONArray() .put("playlists_ids", getCachedPlaylistsIds());
.put(getUserId() + "_-1"));
} }
public static JSONObject getCatalogHeader() throws JSONException { public static JSONObject getCatalogHeader(String text) throws JSONException {
return new JSONObject() return new JSONObject()
.put("id", "cache") .put("id", "cache")
.put("data_type", "none") .put("data_type", "none")
.put("layout", new JSONObject() .put("layout", new JSONObject()
.put("name", "header") .put("name", "header")
.put("title", AndroidUtils.getString(R.string.cached_tracks_title))); .put("title", text));
} }
public static JSONObject getCatalogSeparator() throws JSONException { public static JSONObject getCatalogSeparator() throws JSONException {
@@ -8,6 +8,8 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.delegate.MusicCacheDbDelegate;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File; import java.io.File;
@@ -22,7 +24,7 @@ public class TracklistHelper {
} }
public static List<MusicTrack> getTracks() { public static List<MusicTrack> getTracks() {
List<MusicTrack> tracks = MusicCacheImpl.getAllTracks(); List<MusicTrack> tracks = MusicCacheImpl.getAllOwnTracks();
boolean doNotinvertOrder = Preferences.getBoolValue("invertCachedTracks", false); boolean doNotinvertOrder = Preferences.getBoolValue("invertCachedTracks", false);
@@ -33,13 +35,8 @@ public class TracklistHelper {
return tracks; return tracks;
} }
public static MusicTrack getTrack(String id) {
public static List<MusicTrack> getTracks(String id) { return MusicCacheDbDelegate.getTrackById(AndroidUtils.getGlobalContext(), id);
return MusicCacheImpl.getAlbumById(id);
}
public static List<MusicTrack> getTracksOne(String id) {
return MusicCacheImpl.getFirstAlbumTrack(id);
} }
public static JSONArray tracksToIds(List<MusicTrack> tracks) { public static JSONArray tracksToIds(List<MusicTrack> tracks) {
@@ -53,7 +53,9 @@ public class PlaylistConverter {
var request = new Request.a() var request = new Request.a()
.b(requestUrl) .b(requestUrl)
.a("Accept-Encoding", "gzip") .a("Accept-Encoding", "gzip")
.a(Headers.a("User-Agent", Network.l.c().a(), "Content-Type", "application/x-www-form-urlencoded; charset=utf-8")).a(); .a("User-Agent", Network.l.c().a())
.a("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
.a();
try { try {
var response = CompletableFuture.supplyAsync(() -> { var response = CompletableFuture.supplyAsync(() -> {
@@ -47,8 +47,9 @@ public class AudioGet {
var request = new Request.a() var request = new Request.a()
.b(requestUrl) .b(requestUrl)
.a("Accept-Encoding", "gzip") .a("Accept-Encoding", "gzip")
.a(Headers.a("User-Agent", Network.l.c().a(), "Content-Type", "application/x-www-form-urlencoded; charset=utf-8")).a(); .a("User-Agent", Network.l.c().a())
.a("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
.a();
try { try {
var response = CompletableFuture.supplyAsync(() -> { var response = CompletableFuture.supplyAsync(() -> {
try (Response resp = Network.b(CLIENT_API).a(request).execute()) { try (Response resp = Network.b(CLIENT_API).a(request).execute()) {
@@ -2,9 +2,12 @@ package ru.vtosters.lite.music.downloader;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.music.interfaces.Callback; import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader; import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
public final class CachedDownloader implements ITrackDownloader { public final class CachedDownloader implements ITrackDownloader {
@@ -14,9 +17,8 @@ public final class CachedDownloader implements ITrackDownloader {
this.origin = origin; this.origin = origin;
} }
@Override @Override
public void download(MusicTrack track, Callback callback) { public void download(MusicTrack track, Callback callback, Playlist playlist) {
origin.download(track, new Callback() { origin.download(track, new Callback() {
@Override @Override
public void onProgress(int progress) { public void onProgress(int progress) {
@@ -25,18 +27,26 @@ public final class CachedDownloader implements ITrackDownloader {
@Override @Override
public void onSuccess() { public void onSuccess() {
new ThumbnailDownloader().download(track, new Callback() { new ThumbnailTrackDownloader().download(track, new Callback() {
@Override public void onProgress(int progress) {} @Override public void onProgress(int progress) {
@Override public void onSuccess() {} }
@Override public void onSuccess() {
}
@Override @Override
public void onFailure(Throwable e) { public void onFailure(Throwable e) {
callback.onFailure(e); callback.onFailure(e);
} }
@Override public void onSizeReceived(long size, long header) {} @Override public void onSizeReceived(long size, long header) {
});
}
}, null);
PlaylistCacheDbDelegate.addTrackToPlaylist(AndroidUtils.getGlobalContext(), playlist.v1(), track.y1());
MusicCacheImpl.addTrack(track); MusicCacheImpl.addTrack(track);
callback.onSuccess(); callback.onSuccess();
} }
@@ -50,6 +60,6 @@ public final class CachedDownloader implements ITrackDownloader {
public void onSizeReceived(long size, long header) { public void onSizeReceived(long size, long header) {
callback.onSizeReceived(size, header); callback.onSizeReceived(size, header);
} }
}); }, playlist);
} }
} }
@@ -20,6 +20,7 @@ import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import com.vk.dto.music.Playlist;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.converter.ts.MpegDemuxer; import ru.vtosters.lite.music.converter.ts.MpegDemuxer;
import ru.vtosters.lite.music.interfaces.Callback; import ru.vtosters.lite.music.interfaces.Callback;
@@ -34,7 +35,7 @@ public final class Mp3Downloader implements ITrackDownloader {
} }
@Override @Override
public void download(MusicTrack track, Callback callback) { public void download(MusicTrack track, Callback callback, Playlist playlist) {
String uri = track.D; String uri = track.D;
if (Objects.requireNonNull(uri).isEmpty()) { if (Objects.requireNonNull(uri).isEmpty()) {
String msg = "link error: " + track.y1() + ", title: " + Mp3Downloader.getTitle(track); String msg = "link error: " + track.y1() + ", title: " + Mp3Downloader.getTitle(track);
@@ -3,23 +3,23 @@ package ru.vtosters.lite.music.downloader;
import android.util.Log; import android.util.Log;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import ru.vtosters.lite.music.interfaces.Callback;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java8.util.concurrent.CompletableFuture; import java8.util.concurrent.CompletableFuture;
import ru.vtosters.lite.music.interfaces.Callback;
public class PlaylistDownloader { public class PlaylistDownloader {
public static CompletableFuture<Void> public static void downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
var outDir = new File(path); var outDir = new File(path);
if (!outDir.exists()) if (!outDir.exists())
if (outDir.mkdirs()) Log.v("PlaylistDownloader", "Directory created"); if (outDir.mkdirs()) Log.v("PlaylistDownloader", "Directory created");
else Log.e("PlaylistDownloader", "Directory creation failed"); else Log.e("PlaylistDownloader", "Directory creation failed");
Callback delegate = new ProgressCallback(callback); Callback delegate = new ProgressCallback(callback);
return CompletableFuture.allOf(playlist CompletableFuture.allOf(playlist
.stream() .stream()
.map(x -> { .map(x -> {
Callback.CompletableFutureCallback d = new Callback.CompletableFutureCallback d = new
@@ -30,14 +30,13 @@ public class PlaylistDownloader {
.toArray(CompletableFuture[]::new)); .toArray(CompletableFuture[]::new));
} }
public static CompletableFuture<Void> cachePlaylist(List<MusicTrack> playlist, Callback callback) { public static void cachePlaylist(List<MusicTrack> playlist, Callback callback, Playlist playlistId) {
Callback delegate = new ProgressCallback(callback); Callback delegate = new ProgressCallback(callback);
return CompletableFuture.allOf(playlist CompletableFuture.allOf(playlist
.stream() .stream()
.map(x -> { .map(x -> {
Callback.CompletableFutureCallback d = new Callback.CompletableFutureCallback d = new Callback.CompletableFutureCallback(delegate);
Callback.CompletableFutureCallback(delegate); TrackDownloader.cacheTrack(x, d, playlistId);
TrackDownloader.cacheTrack(x, d);
return d; return d;
}) })
.toArray(CompletableFuture[]::new)); .toArray(CompletableFuture[]::new));
@@ -0,0 +1,57 @@
package ru.vtosters.lite.music.downloader;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.PlaylistUtils;
import java.io.IOException;
import java.net.URL;
import java.util.List;
public class ThumbnailPlaylistDownloader implements ITrackDownloader {
@Override
public void download(MusicTrack track, Callback callback, Playlist playlist) {
JSONObject json = PlaylistUtils.getThumb(playlist); // get thumbnail photo
if (json == null) {
callback.onFailure(new IOException("No thumbnail found"));
return;
}
try {
List<String> sizes = List.of("600", "1200", "68", "135", "300", "270"); // define the order of sizes to download
JSONArray sizesArray = json.getJSONArray("sizes");
for (String size : sizes) {
for (int i = 0; i < sizesArray.length(); i++) {
JSONObject sizeObj = sizesArray.getJSONObject(i);
if (sizeObj.getInt("width") == Integer.parseInt(size)) {
String url = sizeObj.getString("src");
if (url.isEmpty()) continue;
Log.d("Playlist", "downloading thumb " + url);
downloadThumbnailPlaylist(url, Integer.parseInt(size), playlist.v1());
}
}
}
callback.onSuccess();
} catch (IOException | JSONException e) {
callback.onFailure(e);
throw new RuntimeException(e);
}
}
private static void downloadThumbnailPlaylist(String url, int res, String playlistId) throws IOException {
IOUtils.writeToFile(MusicCacheStorageUtils.getPlaylistThumb(playlistId, res), IOUtils.readFully(new URL(url).openStream()));
}
}
@@ -2,6 +2,7 @@ package ru.vtosters.lite.music.downloader;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -14,10 +15,10 @@ import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.IOUtils; import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
public final class ThumbnailDownloader implements ITrackDownloader { public final class ThumbnailTrackDownloader implements ITrackDownloader {
@Override @Override
public void download(MusicTrack track, Callback callback) { public void download(MusicTrack track, Callback callback, Playlist playlist) {
JSONObject json = track.J(); JSONObject json = track.J();
JSONObject album = json.optJSONObject("album"); JSONObject album = json.optJSONObject("album");
if (album == null) return; if (album == null) return;
@@ -50,5 +51,4 @@ public final class ThumbnailDownloader implements ITrackDownloader {
private static void downloadThumbnail(String url, int res, String trackId) throws IOException { private static void downloadThumbnail(String url, int res, String trackId) throws IOException {
IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId, res), IOUtils.readFully(new URL(url).openStream())); IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId, res), IOUtils.readFully(new URL(url).openStream()));
} }
} }
@@ -7,6 +7,7 @@ import com.vk.dto.music.MusicTrack;
import java.io.File; import java.io.File;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.Playlist;
import ru.vtosters.lite.concurrent.VTExecutors; import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.interfaces.Callback; import ru.vtosters.lite.music.interfaces.Callback;
@@ -21,25 +22,24 @@ public final class TrackDownloader {
public static void downloadTrack(MusicTrack track, String path, Callback callback) { public static void downloadTrack(MusicTrack track, String path, Callback callback) {
File outDir = new File(path); File outDir = new File(path);
if (!outDir.exists()) { if (!outDir.exists()) {
if (outDir.mkdir()) if (outDir.mkdir()) {
Log.v("TrackDownloader", "Directory created"); Log.v("TrackDownloader", "Directory created");
else } else {
Log.e("TrackDownloader", "Directory creation failed"); Log.e("TrackDownloader", "Directory creation failed");
}
} }
var outputFile = new File(outDir, IOUtils.getValidFileName( File outputFile = new File(outDir, IOUtils.getValidFileName(MusicTrackUtils.getArtists(track) + " - " + Mp3Downloader.getTitle(track)) + ".mp3");
MusicTrackUtils.getArtists(track) + " - " + Mp3Downloader.getTitle(track)) + ".mp3");
VTExecutors.getMusicDownloadExecutor().submit(() -> new Mp3Downloader(outputFile).download(track, callback)); VTExecutors.getMusicDownloadExecutor().submit(() -> new Mp3Downloader(outputFile).download(track, callback, null));
} }
public static void cacheTrack(MusicTrack track, Callback callback) { public static void cacheTrack(MusicTrack track, Callback callback, Playlist playlist) {
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) {
return; return;
var outputFile = MusicCacheStorageUtils }
.getTrackFile(LibVKXClient.asId(track));
VTExecutors.getMusicDownloadExecutor().submit(() -> new CachedDownloader( File outputFile = MusicCacheStorageUtils.getTrackFile(LibVKXClient.asId(track));
new Mp3Downloader(outputFile)
).download(track, callback)); VTExecutors.getMusicDownloadExecutor().submit(() -> new CachedDownloader(new Mp3Downloader(outputFile)).download(track, callback, playlist));
} }
} }
@@ -1,6 +1,7 @@
package ru.vtosters.lite.music.interfaces; package ru.vtosters.lite.music.interfaces;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
/** /**
* Interface for track downloaders * Interface for track downloaders
@@ -11,6 +12,7 @@ public interface ITrackDownloader {
* *
* @param track Music track * @param track Music track
* @param callback Callback to call when download status changes * @param callback Callback to call when download status changes
* @param playlist
*/ */
void download(MusicTrack track, Callback callback); void download(MusicTrack track, Callback callback, Playlist playlist);
} }
@@ -1,29 +1,33 @@
package ru.vtosters.lite.ui.fragments; package ru.vtosters.lite.ui.fragments;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.preference.Preference;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.core.dialogs.alert.VkAlertDialog; import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vk.dto.music.Playlist;
import com.vtosters.lite.ui.SummaryListPreference; import com.vtosters.lite.ui.SummaryListPreference;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.hooks.other.ThemesUtils; import ru.vtosters.hooks.other.ThemesUtils;
import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.LastFMScrobbler; import ru.vtosters.lite.music.LastFMScrobbler;
import ru.vtosters.lite.music.cache.MusicCacheImpl; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate;
import ru.vtosters.lite.ui.PreferenceFragmentUtils; import ru.vtosters.lite.ui.PreferenceFragmentUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.LifecycleUtils; import ru.vtosters.lite.utils.LifecycleUtils;
import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public class MusicFragment extends TrackedMaterialPreferenceToolbarFragment { public class MusicFragment extends TrackedMaterialPreferenceToolbarFragment {
private static final ExecutorService executor = Executors.newCachedThreadPool(); private static final ExecutorService executor = Executors.newCachedThreadPool();
@SuppressLint("DefaultLocale")
@Override @Override
public void onCreate(Bundle bundle) { public void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
@@ -50,6 +54,20 @@ public class MusicFragment extends TrackedMaterialPreferenceToolbarFragment {
} }
); );
if (MusicCacheImpl.hasPlaylist()) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"cached_playlists",
"Кешированные плейлисты",
String.format("Скачано плейлистов: %d", MusicCacheImpl.getPlaylists().size()),
null,
preference -> {
cachedPlaylistsDialog(requireContext());
return true;
}
);
}
PreferenceFragmentUtils.addPreference( PreferenceFragmentUtils.addPreference(
getPreferenceScreen(), getPreferenceScreen(),
"audio_download", "audio_download",
@@ -322,6 +340,26 @@ public class MusicFragment extends TrackedMaterialPreferenceToolbarFragment {
.show(); .show();
} }
private static void cachedPlaylistsDialog(Context ctx) {
List<Playlist> playlists = PlaylistCacheDbDelegate.getAllPlaylists(ctx);
String[] playlistNames = new String[playlists.size()];
for (int i = 0; i < playlists.size(); i++) {
playlistNames[i] = playlists.get(i).g; // Assuming Playlist class has a getTitle() method
}
VkAlertDialog.Builder builder = new VkAlertDialog.Builder(ctx);
builder.setTitle("Скачанные плейлисты");
builder.setItems(playlistNames, (dialog, which) -> {
Playlist selectedPlaylist = playlists.get(which);
String playlistId = selectedPlaylist.v1();
PlaylistCacheDbDelegate.deletePlaylist(ctx, playlistId);
AndroidUtils.sendToast("Плейлист удален");
});
builder.show();
}
private void delcache(Context ctx) { private void delcache(Context ctx) {
new VkAlertDialog.Builder(ctx) new VkAlertDialog.Builder(ctx)
.setTitle(com.vtosters.lite.R.string.warning) .setTitle(com.vtosters.lite.R.string.warning)
@@ -11,28 +11,36 @@ public class MusicCacheStorageUtils {
} }
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public static File getTrackDirById(final String trackId) { public static File getTrackDirById(String trackId) {
final var res = new File(getCacheStorageDir(), trackId); File res = new File(getCacheStorageDir(), trackId);
res.mkdirs(); res.mkdirs();
return res; return res;
} }
public static File getTrackFile(final String trackId) { public static File getTrackFile(String trackId) {
return new File(getTrackDirById(trackId), "track.mp3"); return new File(getTrackDirById(trackId), "track.mp3");
} }
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public static File getThumbDirById(final String trackId) { public static File getThumbDirById(String trackId) {
final var res = new File(getTrackDirById(trackId), "thumbs"); File res = new File(getTrackDirById(trackId), "thumbs");
res.mkdirs(); res.mkdirs();
return res; return res;
} }
public static File getTrackThumb(final String trackId, final int factor) { public static File getTrackThumb(String trackId, int factor) {
return new File(getThumbDirById(trackId), "thumb_" + factor + ".png"); return new File(getThumbDirById(trackId), "thumb_" + factor + ".png");
} }
public static void removeTrackDirById(final String trackId) { public static File getPlaylistThumb(String playlistId, int factor) {
return new File(getThumbDirById(playlistId), "thumb_playlist_" + factor + ".png");
}
public static void removePlaylistThumb(String playlistId) {
IOUtils.deleteRecursive(getThumbDirById(playlistId));
}
public static void removeTrackDirById(String trackId) {
IOUtils.deleteRecursive(getTrackDirById(trackId)); IOUtils.deleteRecursive(getTrackDirById(trackId));
} }
@@ -0,0 +1,17 @@
package ru.vtosters.lite.utils.music;
import android.util.Log;
import com.vk.dto.music.Playlist;
import org.json.JSONObject;
public class PlaylistUtils {
public static JSONObject getThumb(Playlist playlist) {
try {
Log.d("Playlist", "thumb is not null");
return playlist.F.J();
} catch (Exception e) {
Log.d("Playlist", "thumb is null");
return null;
}
}
}