optimize music cache logic
This commit is contained in:
@@ -10,9 +10,9 @@ import com.vk.music.common.MusicPlaybackLaunchContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
|
||||
import static ru.vtosters.hooks.other.Preferences.getBoolValue;
|
||||
import static ru.vtosters.hooks.other.Preferences.isValidSignature;
|
||||
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
|
||||
|
||||
public class LibVKXClient {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@@ -80,11 +80,6 @@ public class LibVKXClient {
|
||||
lambdaplay(list, musicTrack, iLibVkxService, null);
|
||||
}
|
||||
|
||||
public static String asId(MusicTrack musicTrack) {
|
||||
String id = musicTrack.e + "_" + musicTrack.d;
|
||||
if (musicTrack.O != null && !musicTrack.O.equals("")) {
|
||||
id += "_" + musicTrack.O;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
public static String asId(MusicTrack musicTrack)
|
||||
{ return musicTrack.y1(); }
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import com.vk.api.base.ApiRequest;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.log.L;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.Genius;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ import com.vtosters.lite.R;
|
||||
import com.vtosters.lite.ViewUtils;
|
||||
import com.vtosters.lite.ui.MusicErrorViewHelper;
|
||||
import com.vtosters.lite.ui.holder.RecyclerHolder;
|
||||
import ru.vtosters.lite.music.Genius;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.Genius;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -2,7 +2,7 @@ package ru.vtosters.hooks;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.music.catalog.inject.CatalogJsonInjector;
|
||||
import ru.vtosters.hooks.music.injectors.CatalogJsonInjector;
|
||||
|
||||
public class CatalogsHook {
|
||||
public static JSONObject injectCatalogGetAudio(JSONObject json) {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package ru.vtosters.hooks;
|
||||
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.FileCacheImplementation;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class MusicCacheFilesHook {
|
||||
public static File getTrackFile(String trackId) {
|
||||
return new File(FileCacheImplementation.getTrackFolder(trackId), "track.mp3");
|
||||
}
|
||||
|
||||
public static boolean isTrackExist(String trackId) {
|
||||
return CacheDatabaseDelegate.isCached(trackId);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.hook;
|
||||
package ru.vtosters.hooks.music;
|
||||
|
||||
import android.content.Context;
|
||||
import com.vk.music.fragment.MusicFragment;
|
||||
+19
-33
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.hook;
|
||||
package ru.vtosters.hooks.music;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
|
||||
@@ -8,45 +8,33 @@ import com.vk.music.bottomsheets.a.MusicAction;
|
||||
import com.vk.music.common.MusicPlaybackLaunchContext;
|
||||
import com.vtosters.lite.R;
|
||||
import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.music.VKXUtils;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.*;
|
||||
import static ru.vtosters.hooks.other.Preferences.milkshake;
|
||||
import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.isCached;
|
||||
import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.isVkxCached;
|
||||
import static ru.vtosters.lite.utils.NetworkUtils.isNetworkConnected;
|
||||
|
||||
public class MusicBottomSheetHook {
|
||||
public static ArrayList<MusicAction> hook(ArrayList<MusicAction> actions, MusicTrack musicTrack) {
|
||||
public static ArrayList<MusicAction> hook(final ArrayList<MusicAction> actions,final MusicTrack musicTrack)
|
||||
{
|
||||
if (musicTrack.F1()) return actions;
|
||||
|
||||
var trackid = asId(musicTrack);
|
||||
|
||||
if (isVkxInstalled()) {
|
||||
actions.add(getPlayInVKXAction());
|
||||
final var trackId=asId(musicTrack);
|
||||
if (isVkxInstalled())actions.add(getPlayInVKXAction());
|
||||
if (isIntegrationEnabled())
|
||||
{
|
||||
if(VKXUtils.isVkxCached(trackId))actions.add(getRemoveCacheTrackVkxAction());
|
||||
else if(isNetworkConnected())actions.add(addToCacheTrackVkxAction());
|
||||
}
|
||||
|
||||
if (isIntegrationEnabled()) {
|
||||
if (isVkxCached(trackid)) {
|
||||
actions.add(getRemoveCacheTrackVkxAction());
|
||||
} else if (isNetworkConnected()) {
|
||||
actions.add(addToCacheTrackVkxAction());
|
||||
}
|
||||
} else {
|
||||
if (isCached(trackid)) {
|
||||
actions.add(getRemoveCacheTrackAction());
|
||||
} else if (isNetworkConnected()) {
|
||||
actions.add(addToCacheTrackAction());
|
||||
}
|
||||
else
|
||||
{
|
||||
if(MusicCacheImpl.isCachedTrack(trackId))actions.add(getRemoveCacheTrackAction());
|
||||
else if(isNetworkConnected())actions.add(addToCacheTrackAction());
|
||||
}
|
||||
|
||||
if (isNetworkConnected()) {
|
||||
actions.add(downloadAsMp3Action());
|
||||
}
|
||||
|
||||
if (isNetworkConnected())actions.add(downloadAsMp3Action());
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -54,9 +42,7 @@ public class MusicBottomSheetHook {
|
||||
public static ArrayList<MusicAction> hookDownloadBTN(ArrayList<MusicAction> actions, MusicTrack musicTrack) {
|
||||
if (musicTrack.F1()) return actions;
|
||||
|
||||
var trackid = asId(musicTrack);
|
||||
|
||||
if (isCached(trackid)) {
|
||||
if (MusicCacheImpl.isCachedTrack(musicTrack.y1())) {
|
||||
actions.add(new MusicAction(
|
||||
R.id.music_action_toggle_download,
|
||||
milkshake() ? R.drawable.ic_delete_outline_28 : R.drawable.ic_delete_24,
|
||||
@@ -85,7 +71,7 @@ public class MusicBottomSheetHook {
|
||||
if (isVkxInstalled()) actions.add(getPlayInVKXAction());
|
||||
|
||||
if (isIntegrationEnabled()) {
|
||||
if (isVkxCached(playlist.a, playlist.b)) {
|
||||
if (VKXUtils.isVkxCached(playlist.a, playlist.b)) {
|
||||
actions.add(getRemoveCacheTrackVkxAction());
|
||||
} else if (isNetworkConnected()) {
|
||||
actions.add(addToCacheTrackVkxAction());
|
||||
@@ -124,7 +110,7 @@ public class MusicBottomSheetHook {
|
||||
}
|
||||
|
||||
if (actionId == R.id.remove_from_cache) {
|
||||
CacheDatabaseDelegate.removeTrackFromCache(asId(track));
|
||||
MusicCacheImpl.removeTrack(asId(track));
|
||||
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
|
||||
return true;
|
||||
} else if (actionId == R.id.add_to_cache) {
|
||||
@@ -0,0 +1,14 @@
|
||||
package ru.vtosters.hooks.music;
|
||||
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class MusicCacheFilesHook {
|
||||
public static File getTrackFile(String trackId)
|
||||
{ return MusicCacheStorageUtils.getTrackFile(trackId); }
|
||||
|
||||
public static boolean isTrackExist(String trackId)
|
||||
{ return MusicCacheImpl.isCachedTrack(trackId); }
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.hook;
|
||||
package ru.vtosters.hooks.music;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.AttrRes;
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.catalog.inject;
|
||||
package ru.vtosters.hooks.music.injectors;
|
||||
|
||||
import android.util.Log;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
@@ -12,7 +12,7 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.AccountManagerUtils;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class CatalogJsonInjector {
|
||||
setDefaultAudioPage(oldItems, catalog);
|
||||
}
|
||||
|
||||
if (CacheDatabaseDelegate.hasTracks() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
|
||||
if (!MusicCacheImpl.isEmpty() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
|
||||
var noPlaylists = !json.has("playlists");
|
||||
|
||||
if (noPlaylists) {
|
||||
@@ -91,7 +91,7 @@ public class CatalogJsonInjector {
|
||||
|
||||
fixDailyMix(blocks);
|
||||
|
||||
if (!isUsersCatalog || !CacheDatabaseDelegate.hasTracks() || LibVKXClient.isIntegrationEnabled()) {
|
||||
if (!isUsersCatalog || MusicCacheImpl.isEmpty() || LibVKXClient.isIntegrationEnabled()) {
|
||||
return; // early return if not users catalog or no tracks or integration enabled
|
||||
}
|
||||
|
||||
+6
-8
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.cache.injectors;
|
||||
package ru.vtosters.hooks.music.injectors;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
@@ -12,7 +12,7 @@ import io.reactivex.ObservableOnSubscribe;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
|
||||
import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
|
||||
|
||||
@@ -25,13 +25,11 @@ public class PlaylistInjector {
|
||||
public final static String CHANNEL_NAME = "VTCH";
|
||||
private static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
public static void injectDownloadPlaylist(Playlist playlist) {
|
||||
executor.submit(() -> AudioDownloader.cachePlaylist(playlist));
|
||||
}
|
||||
public static void injectDownloadPlaylist(Playlist playlist)
|
||||
{ executor.submit(() -> AudioDownloader.cachePlaylist(playlist)); }
|
||||
|
||||
public static boolean eligibleForOfflineCaching() {
|
||||
return CacheDatabaseDelegate.hasTracks();
|
||||
}
|
||||
public static boolean eligibleForOfflineCaching()
|
||||
{ return !MusicCacheImpl.isEmpty(); }
|
||||
|
||||
public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) {
|
||||
try {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.cache.injectors;
|
||||
package ru.vtosters.hooks.music.injectors;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.music.model.IpcPlayerModelImpl;
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music.cache.injectors;
|
||||
package ru.vtosters.hooks.music.injectors;
|
||||
|
||||
import android.util.Log;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
@@ -11,7 +11,7 @@ import io.reactivex.ObservableOnSubscribe;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
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.AccountManagerUtils;
|
||||
@@ -56,7 +56,7 @@ public class TracklistInjector {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!CacheDatabaseDelegate.hasTracks()) {
|
||||
if (MusicCacheImpl.isEmpty()) {
|
||||
observableEmitter.b(parser.c(getEmptyCatalog()));
|
||||
} else {
|
||||
observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks())));
|
||||
@@ -135,7 +135,7 @@ public class TracklistInjector {
|
||||
|
||||
button.put("action", action);
|
||||
button.put("block_id", "shuffle");
|
||||
button.put("ref_items_count", CacheDatabaseDelegate.getTrackCount());
|
||||
button.put("ref_items_count", MusicCacheImpl.getTracksCount());
|
||||
button.put("ref_layout_name", "list");
|
||||
button.put("ref_data_type", "music_audios");
|
||||
|
||||
@@ -158,7 +158,7 @@ public class TracklistInjector {
|
||||
|
||||
JSONArray blocks = firstSection.getJSONArray("blocks");
|
||||
|
||||
if (CacheDatabaseDelegate.hasTracks() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
|
||||
if (!MusicCacheImpl.isEmpty() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
|
||||
boolean noPlaylists = hasNoPlaylists(blocks);
|
||||
|
||||
if (noPlaylists) {
|
||||
@@ -1,24 +1,24 @@
|
||||
package ru.vtosters.lite.downloaders;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import com.vk.core.util.ToastUtils;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.dto.music.Playlist;
|
||||
import com.vtosters.lite.R;
|
||||
import ru.vtosters.hooks.MusicCacheFilesHook;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.hooks.music.MusicCacheFilesHook;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.callback.MusicCallbackBuilder;
|
||||
import ru.vtosters.lite.music.converter.playlist.PlaylistConverter;
|
||||
import ru.vtosters.lite.music.downloader.AudioGet;
|
||||
import ru.vtosters.lite.music.downloader.PlaylistDownloader;
|
||||
import ru.vtosters.lite.music.downloader.TrackDownloader;
|
||||
import ru.vtosters.lite.music.downloader.*;
|
||||
import ru.vtosters.lite.music.notification.MusicNotificationBuilder;
|
||||
import ru.vtosters.lite.utils.AccountManagerUtils;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@@ -33,7 +33,8 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
|
||||
public class AudioDownloader {
|
||||
public static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
public static void downloadPlaylist(Playlist playlist) {
|
||||
public static void downloadPlaylist(Playlist playlist)
|
||||
{
|
||||
var tracks = PlaylistConverter.getPlaylist(playlist);
|
||||
|
||||
var playlistName = IOUtils.getValidFileName(playlist.g);
|
||||
@@ -53,15 +54,14 @@ public class AudioDownloader {
|
||||
}
|
||||
|
||||
|
||||
public static void downloadAudio(MusicTrack track) {
|
||||
downloadM3U8(track, false);
|
||||
}
|
||||
public static void downloadAudio(MusicTrack track)
|
||||
{ downloadM3U8(track,false); }
|
||||
|
||||
public static void cacheTrack(MusicTrack track) {
|
||||
var trackId = track.y1();
|
||||
var trackId = LibVKXClient.asId(track);
|
||||
|
||||
if (CacheDatabaseDelegate.isCached(trackId)) {
|
||||
CacheDatabaseDelegate.removeTrackFromCache(LibVKXClient.asId(track));
|
||||
if (MusicCacheImpl.isCachedTrack(trackId)) {
|
||||
MusicCacheImpl.removeTrack(trackId);
|
||||
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.json.JSONObject;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -47,7 +47,7 @@ public class LastFMScrobbler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Preferences.autocache() && !CacheDatabaseDelegate.isCached(uid)) {
|
||||
if (Preferences.autocache() && !MusicCacheImpl.isCachedTrack(uid)) {
|
||||
if (LibVKXClient.isIntegrationEnabled()) {
|
||||
LibVKXClient.getInstance().runOnService(service -> {
|
||||
try {
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package ru.vtosters.lite.music;
|
||||
|
||||
import android.util.Log;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.converter.ts.FFMpeg;
|
||||
import ru.vtosters.lite.music.converter.ts.TSMerger;
|
||||
import ru.vtosters.lite.music.downloader.ThumbnailDownloader;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class M3UDownloader implements ITrackDownloader {
|
||||
private static final OkHttpClient client = VtOkHttpClient.getInstance();
|
||||
|
||||
public static M3UDownloader getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
public static String getTitle(MusicTrack track) {
|
||||
String title = null;
|
||||
|
||||
if (track.f != null) {
|
||||
title = track.f;
|
||||
if (!track.g.isEmpty()) {
|
||||
title += " (" + track.g + ")";
|
||||
}
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
public void downloadTrack(MusicTrack track, File outDir, Callback callback, boolean cache) {
|
||||
if (track.D.isEmpty()) {
|
||||
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
|
||||
callback.onFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Request request = new Request.a().b(track.D).a();
|
||||
String response = client.a(request).execute().a().g();
|
||||
parse(response, outDir, callback, track, cache);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
callback.onFailure();
|
||||
}
|
||||
}
|
||||
|
||||
private void parse(String payload, File outDir, Callback callback, MusicTrack track, boolean cache) throws UnsupportedEncodingException {
|
||||
if (cache) downloadThumbnails(track);
|
||||
|
||||
VKM3UParser parser = new VKM3UParser(track.D.substring(0, track.D.lastIndexOf("/") + 1), payload);
|
||||
AtomicInteger progress = new AtomicInteger(0);
|
||||
List<CompletableFuture<Void>> futures = createFutures(parser, progress, callback, track);
|
||||
|
||||
var resultTs = getResultTsFile(track);
|
||||
var resultMp3 = getResultMp3File(outDir, cache, track);
|
||||
|
||||
callback.onProgress(5);
|
||||
var future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
|
||||
future.join();
|
||||
future.exceptionally(throwable -> {
|
||||
callback.onFailure();
|
||||
return null;
|
||||
}).thenApply(v -> TSMerger.merge(getTsesDir(track), resultTs))
|
||||
.thenApply(mergeResult -> mergeResult && FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track))
|
||||
.thenApply(convertResult -> {
|
||||
if (convertResult)
|
||||
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / parser.mTransportStreams.size()));
|
||||
else callback.onFailure();
|
||||
return convertResult;
|
||||
}).thenRun(() -> IOUtils.deleteRecursive(getTsesDir(track))).thenRun(() -> {
|
||||
callback.onSuccess();
|
||||
if (cache) CacheDatabaseDelegate.insertTrack(track);
|
||||
});
|
||||
}
|
||||
|
||||
private void downloadThumbnails(MusicTrack track) {
|
||||
try {
|
||||
ThumbnailDownloader.downloadThumbnails(track);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private List<CompletableFuture<Void>> createFutures(VKM3UParser parser, AtomicInteger progress, Callback callback, MusicTrack track) {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(parser.mTransportStreams.size());
|
||||
|
||||
var tses = parser.mTransportStreams;
|
||||
for (var i = 0; i < tses.size(); ++i) {
|
||||
final var ts = tses.get(i);
|
||||
final var filename = i + ".ts";
|
||||
futures.add(CompletableFuture.runAsync(() -> processTransportStream(parser, ts, filename, progress, tses.size(), callback, track)));
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
|
||||
private void processTransportStream(VKM3UParser parser, TransportStream ts, String filename, AtomicInteger progress, int size, Callback callback, MusicTrack track) {
|
||||
try {
|
||||
InputStream mediaIs = IOUtils.openStream(ts.getMediaSegmentUri());
|
||||
byte[] content;
|
||||
if (ts.needDecoding()) {
|
||||
InputStream keyPubIs = IOUtils.openStream(ts.getKeyURL());
|
||||
content = IOUtils.decodeStream(mediaIs, IOUtils.readFully(keyPubIs));
|
||||
keyPubIs.close();
|
||||
} else {
|
||||
content = IOUtils.readFully(mediaIs);
|
||||
}
|
||||
File tsDump = new File(getTsesDir(track), filename);
|
||||
IOUtils.writeToFile(tsDump, content);
|
||||
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
|
||||
callback.onSizeReceived((long) content.length * size, parser.mHeapSize);
|
||||
mediaIs.close();
|
||||
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException |
|
||||
InvalidAlgorithmParameterException | InvalidKeyException e) {
|
||||
callback.onFailure();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getTsesDir(MusicTrack track) {
|
||||
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
|
||||
var tsesDir = new File(filesDir, String.valueOf(track.d));
|
||||
tsesDir.mkdirs();
|
||||
return tsesDir;
|
||||
}
|
||||
|
||||
private File getResultTsFile(MusicTrack track) {
|
||||
return new File(getTsesDir(track), "result.ts");
|
||||
}
|
||||
|
||||
private File getResultMp3File(File outDir, boolean cache, MusicTrack track) {
|
||||
var fileName = IOUtils.getValidFileName((cache ? "track" : track.C + " - " + getTitle(track)) + ".mp3");
|
||||
return new File(outDir, fileName);
|
||||
}
|
||||
|
||||
// Initialization-on-demand
|
||||
private static class Holder {
|
||||
private static final M3UDownloader INSTANCE = new M3UDownloader();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package ru.vtosters.lite.music;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
public class TransportStream {
|
||||
|
||||
private final String mMediaSegmentUri;
|
||||
private String mKeyUri;
|
||||
private String mMediaSegmentName;
|
||||
|
||||
public TransportStream(String baseURI, String mediaSegmentName) {
|
||||
this.mMediaSegmentUri = baseURI + mediaSegmentName;
|
||||
}
|
||||
|
||||
public TransportStream(String keyURI, String baseURI, String mediaSegmentName) {
|
||||
this.mKeyUri = keyURI;
|
||||
this.mMediaSegmentUri = baseURI + mediaSegmentName;
|
||||
this.mMediaSegmentName = mediaSegmentName;
|
||||
}
|
||||
|
||||
public boolean needDecoding() {
|
||||
return !TextUtils.isEmpty(mKeyUri);
|
||||
}
|
||||
|
||||
public String getKeyURL() {
|
||||
return mKeyUri;
|
||||
}
|
||||
|
||||
public String getMediaSegmentUri() {
|
||||
return this.mMediaSegmentUri;
|
||||
}
|
||||
|
||||
public String getMediaSegmentName() {
|
||||
return this.mMediaSegmentName;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package ru.vtosters.lite.music;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* The parser for extended M3U playlist format used by VK.
|
||||
*
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#ref-M3U">M3U</a>
|
||||
**/
|
||||
public class VKM3UParser {
|
||||
//Playlist types
|
||||
static public int PLAYLIST_TYPE_VOD = 0;
|
||||
static public int PLAYLIST_TYPE_EVENT = 1;
|
||||
private final String mData;
|
||||
//region Extended M3U global extensions
|
||||
public List<TransportStream> mTransportStreams = new ArrayList<>();
|
||||
/**
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.1.2">EXT-X-VERSION</a>
|
||||
**/
|
||||
public int mVersion = -1;
|
||||
/**
|
||||
* The EXT-X-ALLOW-CACHE tag is used in protocol version 6 or lower.
|
||||
* If the protocol version is upgraded by VK, we will not support that.
|
||||
**/
|
||||
public boolean mAllowCache = false;
|
||||
/**
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1">EXT-X-TARGETDURATION</a>
|
||||
**/
|
||||
public int mTargetDuration = -1;
|
||||
/**
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2">EXT-X-MEDIA-SEQUENCE</a>
|
||||
**/
|
||||
public int mMediaSequence = -1;
|
||||
/**
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.3">EXT-X-DISCONTINUITY-SEQUENCE</a>
|
||||
**/
|
||||
public int mDiscontinuitySequence = -1;
|
||||
//endregion
|
||||
/**
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.5">EXT-X-PLAYLIST-TYPE</a>
|
||||
**/
|
||||
public int mPlaylistType = -1;
|
||||
/**
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.6">EXT-X-I-FRAMES-ONLY</a>
|
||||
**/
|
||||
public boolean mIFramesOnly = false;
|
||||
public long mHeapSize = 0;
|
||||
|
||||
public VKM3UParser(String baseUri, String data) {
|
||||
mData = data;
|
||||
init(baseUri);
|
||||
}
|
||||
|
||||
public static boolean isTS(String str) {
|
||||
return str.matches(".+\\.ts.*");
|
||||
}
|
||||
|
||||
private void init(String baseUri) {
|
||||
if (TextUtils.isEmpty(mData)) throw new NullPointerException("mData==null");
|
||||
Scanner scanner = new Scanner(mData);
|
||||
String line = scanner.nextLine();
|
||||
if (!"#EXTM3U".equals(line))
|
||||
throw new IllegalStateException(String.format("Unknown initial M3U tag: %s", line));
|
||||
boolean aes128 = false;
|
||||
String keyUri = "";
|
||||
while (scanner.hasNextLine()) {
|
||||
line = scanner.nextLine();
|
||||
if ('#' == line.charAt(0)) {
|
||||
parseTag(line);
|
||||
if (line.startsWith("#EXT-X-KEY:") && line.contains("METHOD=AES-128")) {
|
||||
aes128 = true;
|
||||
keyUri = getKeyUri(line, baseUri);
|
||||
}
|
||||
} else if (isTS(line)) {
|
||||
addTransportStream(aes128, keyUri, baseUri, line);
|
||||
aes128 = false;
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Failed to parse: %s", line));
|
||||
}
|
||||
mHeapSize += line.getBytes().length;
|
||||
}
|
||||
}
|
||||
|
||||
private void parseTag(String line) {
|
||||
if (line.startsWith("#EXT-X-VERSION:"))
|
||||
mVersion = Integer.parseInt(line.substring(15));
|
||||
else if (line.startsWith("#EXT-X-ALLOW-CACHE:"))
|
||||
mAllowCache = "YES".equalsIgnoreCase(line.substring(19));
|
||||
else if (line.startsWith("#EXT-X-TARGETDURATION:"))
|
||||
mTargetDuration = Integer.parseInt(line.substring(22));
|
||||
else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE:"))
|
||||
mMediaSequence = Integer.parseInt(line.substring(22));
|
||||
else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:"))
|
||||
mDiscontinuitySequence = Integer.parseInt(line.substring(30));
|
||||
else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:"))
|
||||
parsePlaylistType(line.substring(21));
|
||||
else if (line.startsWith("#EXT-X-I-FRAMES-ONLY"))
|
||||
mIFramesOnly = true;
|
||||
}
|
||||
|
||||
private void parsePlaylistType(String type) {
|
||||
switch (type) {
|
||||
case "VOD" -> mPlaylistType = PLAYLIST_TYPE_VOD;
|
||||
case "EVENT" -> mPlaylistType = PLAYLIST_TYPE_EVENT;
|
||||
default -> throw new IllegalStateException(String.format("Unknown playlist type: %s", type));
|
||||
}
|
||||
}
|
||||
|
||||
private String getKeyUri(String line, String baseUri) {
|
||||
String substr = line.substring(11);
|
||||
substr = substr.substring(substr.indexOf('"') + 1, substr.lastIndexOf('"'));
|
||||
//TODO: supposed endpoint
|
||||
return substr.startsWith("http") ? substr : baseUri + substr;
|
||||
}
|
||||
|
||||
private void addTransportStream(boolean aes128, String keyUri, String baseUri, String line) {
|
||||
TransportStream ts = aes128
|
||||
? new TransportStream(keyUri, baseUri, line)
|
||||
: new TransportStream(baseUri, line);
|
||||
mTransportStreams.add(ts);
|
||||
}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
package ru.vtosters.lite.music.cache;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.RemoteException;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
|
||||
import com.vk.core.util.ToastUtils;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vtosters.lite.R;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static ru.vtosters.lite.music.cache.FileCacheImplementation.getCacheDir;
|
||||
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
|
||||
import static ru.vtosters.lite.utils.AndroidUtils.sendToast;
|
||||
import static ru.vtosters.lite.utils.IOUtils.deleteRecursive;
|
||||
|
||||
public class CacheDatabaseDelegate {
|
||||
private static final int DB_VERSION = 0x1;
|
||||
private static final String DB_NAME = "vt_lite_cache.db";
|
||||
private static final String TABLE_NAME = "tracks";
|
||||
private static final String COLUMN_ID = "id";
|
||||
private static final String COLUMN_TRACK_ID = "trackId";
|
||||
private static final String COLUMN_ALBUM_ID = "albumId";
|
||||
private static final String COLUMN_TITLE = "title";
|
||||
private static final String COLUMN_SUBTITLE = "subtitle";
|
||||
private static final String COLUMN_ARTIST = "artist";
|
||||
private static final String COLUMN_ALBUM_TITLE = "albumTitle";
|
||||
private static final String COLUMN_EXPLICIT = "explicit";
|
||||
private static final String COLUMN_HAS_ARTWORK = "hasArtwork";
|
||||
private static final String COLUMN_DURATION = "duration";
|
||||
private static DbHelper helperInstance;
|
||||
|
||||
protected static DbHelper getHelper() {
|
||||
if (helperInstance == null) helperInstance = new DbHelper(getGlobalContext());
|
||||
return helperInstance;
|
||||
}
|
||||
|
||||
// You MUST call this method ONLY AFTER TRACK IS DOWNLOADED!
|
||||
public static void insertTrack(MusicTrack track) {
|
||||
try {
|
||||
var json = track.J();
|
||||
var albumNode = json.optJSONObject("album");
|
||||
|
||||
var hasArtwork = false;
|
||||
var albumId = -1;
|
||||
var albumName = "";
|
||||
|
||||
if (albumNode != null) {
|
||||
hasArtwork = albumNode.has("thumb");
|
||||
albumId = albumNode.getInt("id");
|
||||
albumName = albumNode.getString("title");
|
||||
}
|
||||
|
||||
var formula = String.format(
|
||||
"INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES ('%s', %s, '%s', '%s', '%s', '%s', %s, %s, %s)",
|
||||
TABLE_NAME,
|
||||
COLUMN_TRACK_ID, COLUMN_ALBUM_ID, COLUMN_TITLE, COLUMN_SUBTITLE, COLUMN_ARTIST, COLUMN_ALBUM_TITLE, COLUMN_EXPLICIT, COLUMN_DURATION, COLUMN_HAS_ARTWORK,
|
||||
track.y1(),
|
||||
albumId,
|
||||
URLEncoder.encode(json.getString("title"), "UTF-8"),
|
||||
URLEncoder.encode(json.optString("subtitle"), "UTF-8"),
|
||||
URLEncoder.encode(json.getString("artist"), "UTF-8"),
|
||||
URLEncoder.encode(albumName, "UTF-8"),
|
||||
json.optInt("is_explicit"),
|
||||
json.getInt("duration"),
|
||||
hasArtwork ? 1 : 0
|
||||
);
|
||||
getHelper().getReadableDatabase().execSQL(formula);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ToastUtils.a(AndroidUtils.getString(R.string.track_bd_insert_err) + " [" + e.getMessage() + "]", true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void insertTracks(List<MusicTrack> tracks) {
|
||||
for (var track : tracks) insertTrack(track);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
getHelper().getReadableDatabase().execSQL(String.format("DELETE FROM %s", TABLE_NAME));
|
||||
deleteRecursive(getCacheDir());
|
||||
sendToast(AndroidUtils.getString(R.string.cached_tracks_deleted));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static JSONObject reconstructFromDb(Cursor cursor) {
|
||||
try {
|
||||
var trackId = cursor.getString(getColumnIndex(cursor, COLUMN_TRACK_ID));
|
||||
var splits = trackId.split("_");
|
||||
var id = Integer.parseInt(splits[1]);
|
||||
var ownerId = Integer.parseInt(splits[0]);
|
||||
|
||||
return new JSONObject()
|
||||
.put("url", FileCacheImplementation.getServerPath())
|
||||
.put("id", id)
|
||||
.put("owner_id", ownerId)
|
||||
.put("title", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_TITLE)), "UTF-8"))
|
||||
.put("subtitle", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_SUBTITLE)), "UTF-8"))
|
||||
.put("artist", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_ARTIST)), "UTF-8"))
|
||||
.put("duration", cursor.getInt(getColumnIndex(cursor, COLUMN_DURATION)))
|
||||
.put("is_explicit", cursor.getInt(getColumnIndex(cursor, COLUMN_EXPLICIT)))
|
||||
.put("access_key", 2281488)
|
||||
.putOpt("album", reconstructAlbumFromDb(cursor, trackId));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return new JSONObject();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject reconstructAlbumFromDb(Cursor cursor, String trackId) {
|
||||
try {
|
||||
if (!FileCacheImplementation.getTrackThumbnail(trackId, 300).exists()
|
||||
&& !FileCacheImplementation.getTrackThumbnail(trackId, 600).exists()
|
||||
&& !FileCacheImplementation.getTrackThumbnail(trackId, 1200).exists()
|
||||
) {
|
||||
var ownerId = Integer.parseInt(trackId.split("_")[1]);
|
||||
return new JSONObject()
|
||||
.put("id", cursor.getInt(getColumnIndex(cursor, COLUMN_ALBUM_ID)))
|
||||
.put("owner_id", ownerId)
|
||||
.put("title", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_ALBUM_TITLE)), "UTF-8"))
|
||||
.putOpt("thumb", reconstructAlbumThumbFromDb(cursor, trackId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject reconstructAlbumThumbFromDb(Cursor cursor, String trackId) {
|
||||
try {
|
||||
if (cursor.getInt(getColumnIndex(cursor, COLUMN_HAS_ARTWORK)) != 0)
|
||||
new JSONObject()
|
||||
.put("height", 600)
|
||||
.put("width", 600)
|
||||
.putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300)))
|
||||
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600)))
|
||||
.putOpt("photo_1200", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 1200)));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int getColumnIndex(Cursor cursor, String name) {
|
||||
return cursor.getColumnIndexOrThrow(name);
|
||||
}
|
||||
|
||||
public static boolean hasTracks() {
|
||||
if (LibVKXClient.isIntegrationEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var formula = String.format(
|
||||
"SELECT COUNT(%s) FROM %s",
|
||||
COLUMN_ID,
|
||||
TABLE_NAME
|
||||
);
|
||||
var cursor = getHelper().getReadableDatabase().rawQuery(formula, null);
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
int count = cursor.getInt(0);
|
||||
cursor.close();
|
||||
return count > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracks() {
|
||||
return getTracks(String.format(
|
||||
"SELECT * FROM %s",
|
||||
TABLE_NAME
|
||||
));
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracksAsPlaylist() {
|
||||
return getTracks(String.format(
|
||||
"SELECT * FROM %s WHERE %s > -1 GROUP BY %s HAVING COUNT(*) > 1 ORDER by id DESC",
|
||||
TABLE_NAME,
|
||||
COLUMN_ALBUM_ID,
|
||||
COLUMN_ALBUM_ID
|
||||
));
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracksByAlbum(String id) {
|
||||
return getTracks(String.format(
|
||||
"SELECT COUNT(%s) FROM %s WHERE %s = %s",
|
||||
COLUMN_ID,
|
||||
TABLE_NAME,
|
||||
COLUMN_ALBUM_ID,
|
||||
id
|
||||
));
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracksByAlbumOne(String id) {
|
||||
return getTracks(String.format(
|
||||
"SELECT * FROM %s WHERE %s = %s LIMIT 1",
|
||||
TABLE_NAME,
|
||||
COLUMN_ALBUM_ID,
|
||||
id
|
||||
));
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracks(String query) {
|
||||
List<MusicTrack> list = new ArrayList<>();
|
||||
try (Cursor cursor = getHelper().getReadableDatabase().rawQuery(query, new String[0])) {
|
||||
while (cursor.moveToNext()) {
|
||||
list.add(new MusicTrack(reconstructFromDb(cursor)));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static boolean isCached(String trackId) {
|
||||
var formula = String.format(
|
||||
"SELECT COUNT(%s) FROM %s WHERE %s = '%s'",
|
||||
COLUMN_ID,
|
||||
TABLE_NAME,
|
||||
COLUMN_TRACK_ID,
|
||||
trackId
|
||||
);
|
||||
try {
|
||||
Cursor size = getHelper().getReadableDatabase().rawQuery(formula, null);
|
||||
if (size != null) {
|
||||
size.moveToFirst();
|
||||
int count = size.getInt(0);
|
||||
size.close();
|
||||
return count != 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isVkxCached(int playlist_id, int owner_id) {
|
||||
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
|
||||
@Override
|
||||
public Boolean run(ILibVkxService service) {
|
||||
try {
|
||||
return service.isPlaylistCached(playlist_id, owner_id);
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean defaultValue() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isVkxCached(String trackId) {
|
||||
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
|
||||
@Override
|
||||
public Boolean run(ILibVkxService service) {
|
||||
try {
|
||||
var splits = trackId.split("_");
|
||||
var id = Integer.parseInt(splits[1]);
|
||||
var owner = Integer.parseInt(splits[0]);
|
||||
return service.isTrackCached(id, owner);
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean defaultValue() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void removeTrackFromCache(String trackId) {
|
||||
deleteRecursive(FileCacheImplementation.getTrackFolder(trackId));
|
||||
|
||||
var formula = String.format(
|
||||
"DELETE FROM %s WHERE %s = '%s'",
|
||||
TABLE_NAME,
|
||||
COLUMN_TRACK_ID,
|
||||
trackId
|
||||
);
|
||||
getHelper().getWritableDatabase().execSQL(formula);
|
||||
}
|
||||
|
||||
public static long getTrackCount() {
|
||||
if (!LibVKXClient.isIntegrationEnabled())
|
||||
return DatabaseUtils.queryNumEntries(getHelper().getReadableDatabase(), TABLE_NAME);
|
||||
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<Long>() {
|
||||
@Override
|
||||
public Long run(ILibVkxService service) {
|
||||
try {
|
||||
return (long) service.getCache().size();
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
return defaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long defaultValue() {
|
||||
return 0L;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class DbHelper extends SQLiteOpenHelper {
|
||||
public DbHelper(Context context) {
|
||||
super(context, DB_NAME, null, DB_VERSION);
|
||||
}
|
||||
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
var formula = String.format(
|
||||
"CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s TEXT, %s INTEGER, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s INTEGER, %s INTEGER, %s INTEGER)",
|
||||
TABLE_NAME,
|
||||
COLUMN_ID, COLUMN_TRACK_ID, COLUMN_ALBUM_ID, COLUMN_TITLE, COLUMN_SUBTITLE, COLUMN_ARTIST, COLUMN_ALBUM_TITLE, COLUMN_EXPLICIT, COLUMN_DURATION, COLUMN_HAS_ARTWORK
|
||||
);
|
||||
db.execSQL(formula);
|
||||
}
|
||||
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package ru.vtosters.lite.music.cache;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.hooks.MusicCacheFilesHook;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FileCacheImplementation {
|
||||
|
||||
public static void clear() {
|
||||
CacheDatabaseDelegate.clear();
|
||||
IOUtils.deleteRecursive(getCacheDir(), false);
|
||||
}
|
||||
|
||||
public static File getCacheDir() {
|
||||
return AndroidUtils.getGlobalContext().getExternalFilesDir("vt_tracks");
|
||||
}
|
||||
|
||||
public static File getTrackFolder(String trackId) {
|
||||
var dir = new File(getCacheDir(), trackId);
|
||||
dir.mkdirs();
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static File getThumbnailsFolder(String trackId) {
|
||||
var dir = new File(getTrackFolder(trackId), "thumbs");
|
||||
dir.mkdirs();
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static File getTrackFile(MusicTrack track) {
|
||||
return MusicCacheFilesHook.getTrackFile(track.y1());
|
||||
}
|
||||
|
||||
public static File getTrackThumbnail(String trackId, int factor) {
|
||||
return new File(getThumbnailsFolder(trackId), "thumb_" + factor + ".png");
|
||||
}
|
||||
|
||||
public static String getFileUri(File file) {
|
||||
if (!file.exists()) return null;
|
||||
return Uri.fromFile(file).toString();
|
||||
}
|
||||
|
||||
public static String getServerPath() {
|
||||
return "https://vtosters.app";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package ru.vtosters.lite.music.cache;
|
||||
|
||||
import android.content.Context;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.cache.db.MusicCacheDb;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MusicCacheDbDelegate
|
||||
{
|
||||
static synchronized private MusicCacheDb connectToDb(final Context context)
|
||||
{
|
||||
synchronized(MusicCacheDb.class)
|
||||
{ return new MusicCacheDb(context); }
|
||||
}
|
||||
|
||||
static public void addTrack(final Context context,
|
||||
final String trackId,
|
||||
final String albumId,
|
||||
final String title,
|
||||
final String subtitle,
|
||||
final String artist,
|
||||
final String albumTitle,
|
||||
final boolean explicit,
|
||||
final int duration,
|
||||
final boolean hasArtwork)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{
|
||||
db.addTrack(
|
||||
trackId,
|
||||
albumId,
|
||||
title,
|
||||
subtitle,
|
||||
artist,
|
||||
albumTitle,
|
||||
explicit,
|
||||
duration,
|
||||
hasArtwork);
|
||||
}
|
||||
}
|
||||
|
||||
static public void removeTrack(final Context context,final String trackId)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ db.deleteTrack(trackId); }
|
||||
}
|
||||
|
||||
static public List< MusicTrack > getAllTracks(final Context context)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ return db.getAllTracks(); }
|
||||
}
|
||||
|
||||
static public List< MusicTrack > getAlbumById(final Context context,final String albumId)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ return db.getAlbum(albumId); }
|
||||
}
|
||||
|
||||
static public List< MusicTrack > getFirstAlbumTrack(final Context context,final String albumId)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ return db.getFirstAlbumTrack(albumId); }
|
||||
}
|
||||
|
||||
static public List< MusicTrack > getPlaylist(final Context context)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ return db.getPlaylist(); }
|
||||
}
|
||||
|
||||
static public long getTracksCount(final Context context)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ return db.getTracksCount(); }
|
||||
}
|
||||
|
||||
static public boolean isCachedTrack(final Context context,final String trackId)
|
||||
{
|
||||
try(final var db=connectToDb(context))
|
||||
{ return db.isCachedTrack(trackId); }
|
||||
}
|
||||
|
||||
static public void drop(final Context context)
|
||||
{ context.deleteDatabase(MusicCacheDb.Constants.DB_NAME); }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ru.vtosters.lite.music.cache;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MusicCacheImpl
|
||||
{
|
||||
static public void addTrack(final MusicTrack track)
|
||||
{
|
||||
MusicCacheDbDelegate.addTrack(
|
||||
AndroidUtils.getGlobalContext(),
|
||||
track.y1(),
|
||||
track.I!=null?track.I.getId()+"":"-1",
|
||||
track.f,
|
||||
track.g,
|
||||
track.C,
|
||||
track.I!=null?track.I.getTitle():"",
|
||||
track.K,
|
||||
track.h,
|
||||
track.I!=null&&track.I.u1()!=null);
|
||||
}
|
||||
|
||||
static public void removeTrack(final String trackId)
|
||||
{
|
||||
MusicCacheDbDelegate.removeTrack(AndroidUtils.getGlobalContext(),trackId);
|
||||
MusicCacheStorageUtils.removeTrackDirById(trackId);
|
||||
}
|
||||
|
||||
static public List< MusicTrack > getAllTracks()
|
||||
{ return MusicCacheDbDelegate.getAllTracks(AndroidUtils.getGlobalContext()); }
|
||||
|
||||
static public List< MusicTrack > getAlbumById(final String albumId)
|
||||
{ return MusicCacheDbDelegate.getAlbumById(AndroidUtils.getGlobalContext(),albumId); }
|
||||
|
||||
static public List< MusicTrack > getFirstAlbumTrack(final String albumId)
|
||||
{ return MusicCacheDbDelegate.getFirstAlbumTrack(AndroidUtils.getGlobalContext(),albumId); }
|
||||
|
||||
static public List< MusicTrack > getPlaylist()
|
||||
{ return MusicCacheDbDelegate.getPlaylist(AndroidUtils.getGlobalContext()); }
|
||||
|
||||
static public long getTracksCount()
|
||||
{
|
||||
return !LibVKXClient.isIntegrationEnabled()
|
||||
?MusicCacheDbDelegate.getTracksCount(AndroidUtils.getGlobalContext())
|
||||
:LibVKXClient.getInstance().runOnServiceSync(
|
||||
new LibVKXClientImpl.LibVKXActionGeneric< Long >() {
|
||||
@Override
|
||||
public Long run(final ILibVkxService service)
|
||||
{
|
||||
try
|
||||
{ return (long)service.getCache().size(); }
|
||||
catch(RemoteException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return defaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long defaultValue()
|
||||
{ return 0L; }
|
||||
});
|
||||
}
|
||||
|
||||
static public boolean isCachedTrack(final String trackId)
|
||||
{ return MusicCacheDbDelegate.isCachedTrack(AndroidUtils.getGlobalContext(),trackId); }
|
||||
|
||||
static public boolean isEmpty()
|
||||
{ return !LibVKXClient.isIntegrationEnabled()&&getTracksCount()==0L; }
|
||||
|
||||
static public void clear()
|
||||
{
|
||||
MusicCacheDbDelegate.drop(AndroidUtils.getGlobalContext());
|
||||
MusicCacheStorageUtils.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package ru.vtosters.lite.music.cache.db;
|
||||
|
||||
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.net.Uri;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.vk.dto.music.AlbumLink;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.dto.music.Thumb;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** <p class="note"><strong>Note:</strong> the {@link AutoCloseable} interface was
|
||||
* first added in the {@link android.os.Build.VERSION_CODES#Q} release for the {@link SQLiteOpenHelper} </p> */
|
||||
public class MusicCacheDb
|
||||
extends SQLiteOpenHelper
|
||||
implements AutoCloseable//for pre-Q
|
||||
{
|
||||
public MusicCacheDb(@Nullable Context context)
|
||||
{ super(context,Constants.DB_NAME,null,Constants.DV_VERSION); }
|
||||
|
||||
@Override
|
||||
public void onCreate(final SQLiteDatabase db)
|
||||
{ db.execSQL(Constants.CREATE_QUERY); }
|
||||
|
||||
@Override
|
||||
public void onUpgrade(final SQLiteDatabase db,final int oldVersion,final int newVersion)
|
||||
{
|
||||
if(oldVersion<newVersion)
|
||||
{
|
||||
final var tracks=getTracksWithCursor(db.query(Constants.TABLE_NAME,null,null,null,null,null,null));;
|
||||
db.execSQL(Constants.DROP_QUERY);
|
||||
onCreate(db);
|
||||
for(var track:tracks)
|
||||
{
|
||||
final var vals=new ContentValues();
|
||||
vals.put(Constants.COLUMN_TRACK_ID,track.y1());
|
||||
vals.put(Constants.COLUMN_ALBUM_ID,track.I!=null?track.I.getId()+"":"-1");
|
||||
vals.put(Constants.COLUMN_TITLE,track.f);
|
||||
vals.put(Constants.COLUMN_SUBTITLE,track.g);
|
||||
vals.put(Constants.COLUMN_ARTIST,track.C);
|
||||
vals.put(Constants.COLUMN_ALBUM_TITLE,track.I!=null?track.I.getTitle():"");
|
||||
vals.put(Constants.COLUMN_EXPLICIT,Boolean.compare(track.K,true));
|
||||
vals.put(Constants.COLUMN_DURATION,track.h);
|
||||
vals.put(Constants.COLUMN_HAS_ARTWORK,track.I!=null&&track.I.u1()!=null);
|
||||
db.insert(Constants.TABLE_NAME,null,vals);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addTrack(final String trackId,
|
||||
final String albumId,
|
||||
final String title,
|
||||
final String subtitle,
|
||||
final String artist,
|
||||
final String albumTitle,
|
||||
final boolean explicit,
|
||||
final int duration,
|
||||
final boolean hasArtwork)
|
||||
{
|
||||
final var vals=new ContentValues();
|
||||
vals.put(Constants.COLUMN_TRACK_ID,trackId);
|
||||
vals.put(Constants.COLUMN_ALBUM_ID,albumId);
|
||||
vals.put(Constants.COLUMN_TITLE,title);
|
||||
vals.put(Constants.COLUMN_SUBTITLE,subtitle);
|
||||
vals.put(Constants.COLUMN_ARTIST,artist);
|
||||
vals.put(Constants.COLUMN_ALBUM_TITLE,albumTitle);
|
||||
vals.put(Constants.COLUMN_EXPLICIT,Boolean.compare(explicit,true));
|
||||
vals.put(Constants.COLUMN_DURATION,duration);
|
||||
vals.put(Constants.COLUMN_HAS_ARTWORK,Boolean.compare(hasArtwork,true));
|
||||
final long row=getWritableDatabase().insert(Constants.TABLE_NAME,null,vals);
|
||||
if(AndroidUtils.isDebuggable())Log.d("MusicCacheDb","addTrack(): "+row);
|
||||
}
|
||||
|
||||
public void deleteTrack(final String trackId)
|
||||
{ getWritableDatabase().delete(Constants.TABLE_NAME,Constants.COLUMN_TRACK_ID+" = ?",new String[]{ trackId }); }
|
||||
|
||||
public boolean isCachedTrack(final String trackId)
|
||||
{
|
||||
try(final var cur=getReadableDatabase().query(
|
||||
Constants.TABLE_NAME,
|
||||
new String[]{ Constants.COLUMN_TRACK_ID },
|
||||
Constants.COLUMN_TRACK_ID+" = ?",
|
||||
new String[]{ trackId },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"0,1"))
|
||||
{ return cur.moveToFirst(); }
|
||||
}
|
||||
|
||||
public long getTracksCount()
|
||||
{ return DatabaseUtils.queryNumEntries(getReadableDatabase(),Constants.TABLE_NAME); }
|
||||
|
||||
public List< MusicTrack > getAllTracks()
|
||||
{ return getTracksWithCursor(getReadableDatabase().query(Constants.TABLE_NAME,null,null,null,null,null,null)); }
|
||||
|
||||
public List< MusicTrack > getAlbum(final String albumId)
|
||||
{ return getAlbumTracks(albumId,null); }
|
||||
|
||||
public List< MusicTrack > getFirstAlbumTrack(final String albumId)
|
||||
{ return getAlbumTracks(albumId,"0,1"); }
|
||||
|
||||
private List< MusicTrack > getAlbumTracks(final String albumId,@Nullable final String limit)
|
||||
{
|
||||
return getTracksWithCursor(getReadableDatabase().query(
|
||||
Constants.TABLE_NAME,
|
||||
null,
|
||||
Constants.COLUMN_ALBUM_ID+" = ?",
|
||||
new String[]{ albumId },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
limit));
|
||||
}
|
||||
|
||||
|
||||
public List< MusicTrack > getPlaylist()
|
||||
{
|
||||
return getTracksWithCursor(getWritableDatabase().query(
|
||||
Constants.TABLE_NAME,
|
||||
null,
|
||||
Constants.COLUMN_ALBUM_ID+" > -1",
|
||||
null,
|
||||
Constants.COLUMN_ALBUM_ID,
|
||||
"count(*) > 1",
|
||||
Constants.COLUMN_ID+" desc"));
|
||||
}
|
||||
|
||||
private List< MusicTrack > getTracksWithCursor(final Cursor cur)
|
||||
{
|
||||
final var res=new ArrayList< MusicTrack >();
|
||||
if(cur!=null&&cur.moveToFirst())
|
||||
try(cur)
|
||||
{
|
||||
do
|
||||
{
|
||||
final var track=fromCursor(cur);
|
||||
if(track!=null)res.add(track);
|
||||
}while(cur.moveToNext());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static private MusicTrack fromCursor(final Cursor cur)
|
||||
{
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
final var res=new MusicTrack();
|
||||
res.J=Integer.MAX_VALUE+"";//access_key
|
||||
res.D="https://vtosters.app";//url
|
||||
var i=cur.getColumnIndex(Constants.COLUMN_TRACK_ID);
|
||||
if(i==-1)break;
|
||||
final var trackId=cur.getString(i);
|
||||
final var splits=trackId.split("_");
|
||||
if(splits.length!=2)break;
|
||||
res.d=Integer.parseInt(splits[1]);//id
|
||||
res.e=Integer.parseInt(splits[0]);//owner_id
|
||||
i=cur.getColumnIndex(Constants.COLUMN_TITLE);
|
||||
if(i==-1)break;
|
||||
res.f=URLDecoder.decode(cur.getString(i),"UTF-8");//title
|
||||
i=cur.getColumnIndex(Constants.COLUMN_SUBTITLE);
|
||||
if(i==-1)break;
|
||||
res.g=URLDecoder.decode(cur.getString(i),"UTF-8");//subtitle
|
||||
i=cur.getColumnIndex(Constants.COLUMN_ARTIST);
|
||||
if(i==-1)break;
|
||||
res.C=URLDecoder.decode(cur.getString(i),"UTF-8");//artist
|
||||
i=cur.getColumnIndex(Constants.COLUMN_DURATION);
|
||||
if(i==-1)break;
|
||||
res.h=cur.getInt(i);//duration
|
||||
i=cur.getColumnIndex(Constants.COLUMN_EXPLICIT);
|
||||
if(i==-1)break;
|
||||
res.K=cur.getInt(i)>0;//is_explicit
|
||||
i=cur.getColumnIndex(Constants.COLUMN_ALBUM_ID);
|
||||
if(i==-1)break;
|
||||
final var albumId=Integer.parseInt(cur.getString(i));
|
||||
i=cur.getColumnIndex(Constants.COLUMN_ALBUM_TITLE);
|
||||
final String albumTitle=i!=-1?cur.getString(i):null;
|
||||
final var node=new JSONObject();
|
||||
var f=MusicCacheStorageUtils.getTrackThumb(trackId,300);
|
||||
if(f.exists())node.putOpt("photo_600",Uri.fromFile(f).toString());
|
||||
f=MusicCacheStorageUtils.getTrackThumb(trackId,600);
|
||||
if(f.exists())node.putOpt("photo_600",Uri.fromFile(f).toString());
|
||||
f=MusicCacheStorageUtils.getTrackThumb(trackId,1200);
|
||||
if(f.exists())node.putOpt("photo_1200",Uri.fromFile(f).toString());
|
||||
res.I=new AlbumLink(
|
||||
albumId,
|
||||
res.e,
|
||||
res.J,
|
||||
albumTitle,
|
||||
node.length()>0
|
||||
?new Thumb(node.put("width",600)
|
||||
.put("height",600))
|
||||
:null
|
||||
);//album
|
||||
return res;
|
||||
}while(false);
|
||||
}
|
||||
catch(Throwable ignored)
|
||||
{ }
|
||||
return null;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Constants
|
||||
{
|
||||
int DV_VERSION=0x2;
|
||||
String DB_NAME="vt_lite_cache.db";
|
||||
String TABLE_NAME="tracks";
|
||||
//columns
|
||||
String COLUMN_ID="id";
|
||||
String COLUMN_TRACK_ID="trackId";
|
||||
String COLUMN_ALBUM_ID="albumId";
|
||||
String COLUMN_TITLE="title";
|
||||
String COLUMN_SUBTITLE="subtitle";
|
||||
String COLUMN_ARTIST="artist";
|
||||
String COLUMN_ALBUM_TITLE="albumTitle";
|
||||
String COLUMN_EXPLICIT="explicit";
|
||||
String COLUMN_DURATION="duration";
|
||||
String COLUMN_HAS_ARTWORK="hasArtwork";
|
||||
//queries
|
||||
String CREATE_QUERY="create table "+TABLE_NAME+"("
|
||||
+COLUMN_ID+" INTEGER PRIMARY KEY autoincrement,"
|
||||
+COLUMN_TRACK_ID+" text not null,"
|
||||
+COLUMN_ALBUM_ID+" text not null,"
|
||||
+COLUMN_TITLE+" text not null,"
|
||||
+COLUMN_SUBTITLE+" text not null,"
|
||||
+COLUMN_ARTIST+" text not null,"
|
||||
+COLUMN_ALBUM_TITLE+" text not null,"
|
||||
+COLUMN_EXPLICIT+" int not null,"
|
||||
+COLUMN_DURATION+" int not null,"
|
||||
+COLUMN_HAS_ARTWORK+" int not null"
|
||||
+")";
|
||||
String DROP_QUERY="drop table if exists "+TABLE_NAME;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package ru.vtosters.lite.music.cache.helpers;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.vk.dto.music.Playlist;
|
||||
import com.vtosters.lite.R;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.FileCacheImplementation;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -44,8 +45,8 @@ public class PlaylistHelper {
|
||||
if (!photo.isEmpty())
|
||||
jPhoto.put("height", 600)
|
||||
.put("width", 600)
|
||||
.putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300)))
|
||||
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600)));
|
||||
.putOpt("photo_300",Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId,300)).toString())
|
||||
.putOpt("photo_600",Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId,600)).toString());
|
||||
var obj = new JSONObject()
|
||||
.put("id", -2)
|
||||
.put("owner_id", albumId)
|
||||
@@ -68,7 +69,7 @@ public class PlaylistHelper {
|
||||
|
||||
public static List<Playlist> getAlbumPlaylists() {
|
||||
List<Playlist> list = new ArrayList<>();
|
||||
for (var track : CacheDatabaseDelegate.getTracksAsPlaylist()) {
|
||||
for (var track : MusicCacheImpl.getPlaylist()) {
|
||||
var albumLink = track.I;
|
||||
var thumb = albumLink.u1();
|
||||
list.add(createAlbum(
|
||||
|
||||
+7
-7
@@ -7,8 +7,8 @@ import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.FileCacheImplementation;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
@@ -22,7 +22,7 @@ public class TracklistHelper {
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracks() {
|
||||
List<MusicTrack> tracks = CacheDatabaseDelegate.getTracks();
|
||||
List<MusicTrack> tracks = MusicCacheImpl.getAllTracks();
|
||||
|
||||
boolean doNotinvertOrder = Preferences.getBoolValue("invertCachedTracks", false);
|
||||
|
||||
@@ -35,11 +35,11 @@ public class TracklistHelper {
|
||||
|
||||
|
||||
public static List<MusicTrack> getTracks(String id) {
|
||||
return CacheDatabaseDelegate.getTracksByAlbum(id);
|
||||
return MusicCacheImpl.getAlbumById(id);
|
||||
}
|
||||
|
||||
public static List<MusicTrack> getTracksOne(String id) {
|
||||
return CacheDatabaseDelegate.getTracksByAlbumOne(id);
|
||||
return MusicCacheImpl.getFirstAlbumTrack(id);
|
||||
}
|
||||
|
||||
public static JSONArray tracksToIds(List<MusicTrack> tracks) {
|
||||
@@ -56,7 +56,7 @@ public class TracklistHelper {
|
||||
var arr = new JSONArray();
|
||||
for (MusicTrack track : tracks) {
|
||||
var json = track.J();
|
||||
var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track));
|
||||
var folder = MusicCacheStorageUtils.getThumbDirById(LibVKXClient.asId(track));
|
||||
try {
|
||||
addCachedThumbnails(json, folder);
|
||||
} catch (JSONException | MalformedURLException e) {
|
||||
@@ -71,7 +71,7 @@ public class TracklistHelper {
|
||||
List<MusicTrack> tracks = new ArrayList<>();
|
||||
for (MusicTrack track : list) {
|
||||
var json = track.J();
|
||||
var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track));
|
||||
var folder = MusicCacheStorageUtils.getThumbDirById(LibVKXClient.asId(track));
|
||||
try {
|
||||
addCachedThumbnails(json, folder);
|
||||
} catch (JSONException | MalformedURLException e) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import com.vtosters.lite.R;
|
||||
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
|
||||
import ru.vtosters.lite.music.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
public class MusicCallbackBuilder {
|
||||
|
||||
@@ -26,7 +26,8 @@ import static ru.vtosters.lite.proxy.ProxyUtils.getApi;
|
||||
* stuff for converting playlists DTO
|
||||
*/
|
||||
public class PlaylistConverter {
|
||||
public static List<MusicTrack> getPlaylist(Playlist oldPlaylist) {
|
||||
public static List<MusicTrack> getPlaylist(Playlist oldPlaylist)
|
||||
{
|
||||
var tracks = requestPlaylist(0, oldPlaylist.b, oldPlaylist.Q, oldPlaylist.a);
|
||||
return tracks.stream().filter(track -> !track.D.isEmpty()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
package ru.vtosters.lite.music.converter.ts;
|
||||
|
||||
import android.util.Log;
|
||||
import com.arthenica.ffmpegkit.FFmpegKit;
|
||||
import com.arthenica.ffmpegkit.ReturnCode;
|
||||
import com.vk.core.util.ToastUtils;
|
||||
import com.vk.dto.music.Artist;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vtosters.lite.R;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FFMpeg {
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
public class FFMpeg
|
||||
{
|
||||
|
||||
/**
|
||||
* @param in Merged .ts file via {@link ru.vtosters.lite.music.converter.ts.TSMerger}
|
||||
@@ -25,79 +23,48 @@ public class FFMpeg {
|
||||
* @param track Track to be converted
|
||||
* @return True if success, else false
|
||||
*/
|
||||
public static boolean convert(File in, String out, MusicTrack track) {
|
||||
|
||||
public static void convert(File in, String out, MusicTrack track)
|
||||
throws Throwable
|
||||
{
|
||||
// convert .ts file to .mp3 file via ffmpeg
|
||||
var sb = new StringBuilder();
|
||||
sb.append("-y -i \"").append(in.getAbsolutePath()).append("\"");
|
||||
sb.append(" -map 0 -dn ");
|
||||
//sb.append(" -loglevel error");
|
||||
//sb.append(" -hide_banner");
|
||||
sb.append(" -write_id3v2 1");
|
||||
|
||||
String title = null;
|
||||
|
||||
if (track.f != null) {
|
||||
title = track.f;
|
||||
if (!track.g.isEmpty()) {
|
||||
title += " (" + track.g + ")";
|
||||
}
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
sb.append(" -metadata title=\"").append(IOUtils.getValidFileName(title)).append("\"");
|
||||
}
|
||||
|
||||
if (track.L != null) {
|
||||
sb.append(" -metadata artist=\"").append(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))).append("\"");
|
||||
} else if (track.C != null) {
|
||||
sb.append(" -metadata artist=\"").append(track.C).append("\"");
|
||||
}
|
||||
if (track.I != null) {
|
||||
sb.append(" -metadata album=\"").append(track.I.getTitle()).append("\"");
|
||||
}
|
||||
|
||||
sb.append(" -c copy \"").append(out).append("\"");
|
||||
|
||||
var command = sb.toString();
|
||||
|
||||
var session = FFmpegKit.execute(command);
|
||||
var rc = session.getReturnCode();
|
||||
|
||||
if (ReturnCode.isSuccess(rc)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.e("FFMpeg", "FFmpegKit failed with return code " + rc);
|
||||
return false;
|
||||
final var cmd=" -y -i \""+in.getAbsolutePath()+'"'
|
||||
+" -map 0 -dn"
|
||||
+" -loglevel error"
|
||||
+" -hide_banner"
|
||||
+" -write_id3v2 1"
|
||||
// +" -metadata title=\""+IOUtils.getValidFileName(M3UDownloader.getTitle(track))+'"'
|
||||
// +" -metadata artist=\""+normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", ")))+'"'
|
||||
// +" -metadata album=\""+normalizeMetadata(track.I.getTitle())+'"'
|
||||
+" -c copy \""+out+'"';
|
||||
final var ses=FFmpegKit.execute(cmd);
|
||||
if(ses.getReturnCode().isValueCancel())throw new RuntimeException(ses.getLogsAsString());
|
||||
}
|
||||
|
||||
private static void copyAssets() {
|
||||
try {
|
||||
String[] files = AndroidUtils.getGlobalContext().getAssets().list("ffmpeg");
|
||||
for (String file : files) {
|
||||
if (file.endsWith(".so")) {
|
||||
InputStream in = AndroidUtils.getGlobalContext().getAssets().open("ffmpeg" + File.separator + file);
|
||||
FileOutputStream out = new FileOutputStream(AndroidUtils.getGlobalContext().getFilesDir() + File.separator + file);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
static String normalizeMetadata(String in)
|
||||
{ return in.replace('"','‟'); }
|
||||
|
||||
private static void checkFFMpegLibs() {
|
||||
var lib = new File(AndroidUtils.getGlobalContext().getFilesDir(), "libffmpegkit_abidetect.so");
|
||||
if (!lib.exists()) {
|
||||
ToastUtils.a(R.string.ffmpegkit_copy);
|
||||
copyAssets();
|
||||
}
|
||||
}
|
||||
// private static void copyAssets()
|
||||
// throws IOException
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// for (var filename:AndroidUtils.getGlobalContext().getAssets().list("ffmpeg"))
|
||||
// if(filename.endsWith(".so"))
|
||||
// IOUtils.writeToFile(
|
||||
// new File(AndroidUtils.getGlobalContext().getFilesDir(),filename),
|
||||
// IOUtils.readFully(AndroidUtils.getGlobalContext().getAssets().open("ffmpeg"+File.separator+filename)));
|
||||
// }
|
||||
// catch(IOException e)
|
||||
// { throw e; }
|
||||
// }
|
||||
//
|
||||
// private static void checkFFMpegLibs()
|
||||
// {
|
||||
// var lib=new File(AndroidUtils.getGlobalContext().getFilesDir(),"libffmpegkit_abidetect.so");
|
||||
// if(!lib.exists())
|
||||
// {
|
||||
// ToastUtils.a(R.string.ffmpegkit_copy);
|
||||
// copyAssets();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package ru.vtosters.lite.music.converter.ts;
|
||||
|
||||
import android.os.Build;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TSMerger {
|
||||
/**
|
||||
@@ -17,58 +16,23 @@ public class TSMerger {
|
||||
* @param in Directory with .ts files
|
||||
* @return True if success, else false
|
||||
*/
|
||||
public static boolean merge(File in, File out) {
|
||||
var tsesList = in.listFiles();
|
||||
|
||||
if (tsesList == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var mergedByteArrayStream = new ByteArrayOutputStream();
|
||||
|
||||
sortTsesList(tsesList);
|
||||
|
||||
for (var file : tsesList) {
|
||||
if (file.getName().endsWith(".ts")) {
|
||||
try {
|
||||
mergedByteArrayStream.write(IOUtils.readFully(file));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return writeOutputFile(out, mergedByteArrayStream);
|
||||
public static void merge(final File in,final File out)
|
||||
throws Throwable
|
||||
{
|
||||
var tsFiles=in.listFiles((dir,name) -> name.matches(".*\\d+\\.ts"));
|
||||
if(tsFiles==null||tsFiles.length==0)throw new IOException("Dir "+in.getAbsolutePath()+" doesn't contain indexed \"*.ts\" files");
|
||||
Arrays.sort(tsFiles,Comparator.comparingInt(TSMerger::getFileIndex));
|
||||
final var bis=new ByteArrayOutputStream();
|
||||
for(var f:tsFiles)bis.write(IOUtils.readFully(f));
|
||||
bis.close();
|
||||
IOUtils.writeToFile(out,bis.toByteArray());
|
||||
}
|
||||
|
||||
private static void sortTsesList(File[] tsesList) {
|
||||
Arrays.sort(tsesList, (ts1, ts2) -> {
|
||||
String ts1Name = ts1.getName();
|
||||
String ts2Name = ts2.getName();
|
||||
int i1 = Integer.parseInt(ts1Name.substring(0, ts1Name.indexOf('.', 1)));
|
||||
int i2 = Integer.parseInt(ts2Name.substring(0, ts2Name.indexOf('.', 1)));
|
||||
return Integer.compare(i1, i2);
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
Files.write(out.toPath(), mergedByteArrayStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
try (FileOutputStream fos = new FileOutputStream(out)) {
|
||||
fos.write(mergedByteArrayStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
static int getFileIndex(final File f)
|
||||
{
|
||||
final var p=Pattern.compile(".*(\\d+)\\.ts");
|
||||
final var m=p.matcher(f.getName());
|
||||
if(m.find())return Integer.parseInt(m.group(1));
|
||||
throw new NullPointerException("Filename has no index"+f.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.e;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.f;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.f.a;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
import java8.util.concurrent.CompletionException;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.converter.ts.FFMpeg;
|
||||
import ru.vtosters.lite.music.converter.ts.TSMerger;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class M3UDownloader
|
||||
implements ITrackDownloader
|
||||
{
|
||||
public static M3UDownloader getInstance()
|
||||
{ return Holder.INSTANCE; }
|
||||
|
||||
@Override
|
||||
public void downloadTrack(
|
||||
final MusicTrack track,
|
||||
final File outDir,
|
||||
final Callback callback,
|
||||
final boolean cache)
|
||||
{
|
||||
if (track.D.isEmpty())
|
||||
{
|
||||
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
|
||||
callback.onFailure();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{ parse(outDir,callback,track,cache); }
|
||||
catch(IOException e)
|
||||
{
|
||||
callback.onFailure();
|
||||
Log.e("M3UDownloader",e+"");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static void parse(
|
||||
final File outDir,
|
||||
final Callback callback,
|
||||
final MusicTrack track,
|
||||
final boolean cache)
|
||||
throws IOException
|
||||
{
|
||||
final var parser=new com.google.android.exoplayer2.source.hls.playlist.h(e.a(track.D));
|
||||
final var baseUri=track.D.substring(0,track.D.lastIndexOf("/")+1);
|
||||
final var segments=((f)parser.a(Uri.parse(baseUri),IOUtils.openStream(track.D))).o;
|
||||
final var progress=new AtomicInteger();
|
||||
callback.onProgress(5);
|
||||
final var resultTs=getResultTsFile(track);
|
||||
final var resultMp3=getResultMp3File(outDir, cache, track);
|
||||
try
|
||||
{
|
||||
CompletableFuture.allOf(createFutures(baseUri,segments,progress,callback,track))
|
||||
.thenRun(() ->
|
||||
{
|
||||
if(cache)
|
||||
try
|
||||
{ ThumbnailDownloader.downloadThumbnails(track); }
|
||||
catch(IOException e)
|
||||
{ throw new RuntimeException("Failed to download thumbs",e); }
|
||||
})
|
||||
.thenRun(() ->
|
||||
{
|
||||
try
|
||||
{ TSMerger.merge(getTsesDir(track),resultTs); }
|
||||
catch(Throwable e)
|
||||
{ throw new RuntimeException("Failed to merge ts files",e); }
|
||||
})
|
||||
.thenRun(() ->
|
||||
{
|
||||
try
|
||||
{ FFMpeg.convert(resultTs,resultMp3.getAbsolutePath(),track); }
|
||||
catch(Throwable e)
|
||||
{ throw new RuntimeException("FFmpeg error",e); }
|
||||
})
|
||||
.thenRun(() -> callback.onProgress(10+Math.round(80.0f*progress.addAndGet(1)/segments.size())))
|
||||
.thenRun(() -> MusicCacheImpl.addTrack(track))
|
||||
.whenComplete((unused,tr) -> {
|
||||
if(tr!=null)
|
||||
{
|
||||
IOUtils.deleteRecursive(MusicCacheStorageUtils.getTrackDirById(track.y1()));
|
||||
callback.onFailure();
|
||||
}
|
||||
else callback.onSuccess();
|
||||
IOUtils.deleteRecursive(getTsesDir(track));
|
||||
})
|
||||
.join();
|
||||
}
|
||||
catch(CompletionException e)
|
||||
{ Log.e("M3UDownloader","Failed to download track",e.getCause()); }
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
static CompletableFuture[] createFutures(
|
||||
final String baseUri,
|
||||
final List< a > segments,
|
||||
final AtomicInteger progress,
|
||||
final Callback callback,
|
||||
final MusicTrack track)
|
||||
{
|
||||
final var futures=new CompletableFuture[segments.size()];
|
||||
for(var i=0;i<segments.size();++i)
|
||||
{
|
||||
final var segment=segments.get(i);
|
||||
final var filename=i+".ts";
|
||||
futures[i]=CompletableFuture
|
||||
.runAsync(() -> processSegment(baseUri,segment,filename,progress,segments.size(),callback,track));
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
|
||||
static void processSegment(
|
||||
final String baseUri,
|
||||
final a segment,
|
||||
final String filename,
|
||||
final AtomicInteger progress,
|
||||
final int size,
|
||||
final Callback callback,
|
||||
final MusicTrack track)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buff=IOUtils.readFully(IOUtils.openStream(baseUri+segment.a/*url*/));
|
||||
if(!TextUtils.isEmpty(segment.g))
|
||||
{
|
||||
final var cipher=Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
final var cipherKey=new SecretKeySpec(IOUtils.readFully(IOUtils.openStream(segment.g)),"AES");
|
||||
var cipherBytes=segment.h.getBytes();
|
||||
//if IV doesn't pass, it must be created manually and filled with zeros
|
||||
if(cipherBytes.length!=16)Arrays.fill((cipherBytes=new byte[16]),(byte)0x0);
|
||||
var cipherIv=new IvParameterSpec(cipherBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE,cipherKey,cipherIv);
|
||||
buff=cipher.doFinal(buff);
|
||||
}
|
||||
IOUtils.writeToFile(new File(getTsesDir(track),filename),buff);
|
||||
callback.onProgress(10+Math.round(80.0f*progress.addAndGet(1)/size));
|
||||
}
|
||||
catch(Exception e)
|
||||
{ throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
static public String getTitle(final MusicTrack track)
|
||||
{ return track.f+(!TextUtils.isEmpty(track.g)?+'('+track.g+')':null); }
|
||||
|
||||
static File getTsesDir(final MusicTrack track)
|
||||
{
|
||||
var filesDir=AndroidUtils.getGlobalContext().getFilesDir();
|
||||
var tsesDir=new File(filesDir,String.valueOf(track.d));
|
||||
tsesDir.mkdirs();
|
||||
return tsesDir;
|
||||
}
|
||||
|
||||
static File getResultTsFile(final MusicTrack track)
|
||||
{ return new File(getTsesDir(track),"result.ts"); }
|
||||
|
||||
static File getResultMp3File(final File outDir,final boolean cache,final MusicTrack track)
|
||||
{ return new File(outDir,IOUtils.getValidFileName((cache?"track":track.C+" - "+getTitle(track))+".mp3")); }
|
||||
|
||||
// Initialization-on-demand
|
||||
private static class Holder
|
||||
{
|
||||
private static final M3UDownloader INSTANCE = new M3UDownloader();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -70,7 +70,7 @@
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.Callback callback) {
|
||||
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.interfaces.Callback callback) {
|
||||
//
|
||||
// }
|
||||
//}
|
||||
@@ -2,34 +2,36 @@ package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import android.util.Log;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class PlaylistDownloader {
|
||||
public static void downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
|
||||
File outDir = new File(path);
|
||||
public class PlaylistDownloader
|
||||
{
|
||||
static public void downloadPlaylist(final List< MusicTrack > playlist,
|
||||
final String playlistName,
|
||||
final String path,
|
||||
final Callback callback)
|
||||
{
|
||||
final var outDir=new File(path);
|
||||
if (!outDir.exists())
|
||||
if (outDir.mkdirs())
|
||||
Log.v("PlaylistDownloader", "Directory created");
|
||||
else
|
||||
Log.e("PlaylistDownloader", "Directory creation failed");
|
||||
|
||||
var downloadedTracks = new AtomicInteger(0);
|
||||
|
||||
for (MusicTrack musicTrack : playlist) {
|
||||
TrackDownloader.downloadTrack(musicTrack, path, new Callback() {
|
||||
if(outDir.mkdirs())Log.v("PlaylistDownloader","Directory created");
|
||||
else Log.e("PlaylistDownloader","Directory creation failed");
|
||||
final var downloadedTracks=new AtomicInteger();
|
||||
for (var musicTrack:playlist)
|
||||
TrackDownloader.downloadTrack(musicTrack, path, new Callback()
|
||||
{
|
||||
@Override
|
||||
public void onProgress(int progress) {
|
||||
}
|
||||
public void onProgress(int progress)
|
||||
{ }
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
var currentProgress = downloadedTracks.incrementAndGet();
|
||||
callback.onProgress(currentProgress);
|
||||
}
|
||||
public void onSuccess()
|
||||
{ callback.onProgress(downloadedTracks.incrementAndGet()); }
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
@@ -37,36 +39,32 @@ public class PlaylistDownloader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSizeReceived(long size, long header) {
|
||||
}
|
||||
public void onSizeReceived(long size, long header)
|
||||
{ }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void cachePlaylist(List<MusicTrack> playlist, Callback callback) {
|
||||
var downloadedTracks = new AtomicInteger(0);
|
||||
|
||||
for (MusicTrack musicTrack : playlist) {
|
||||
TrackDownloader.cacheTrack(musicTrack, new Callback() {
|
||||
static public void cachePlaylist(final List< MusicTrack > playlist,final Callback callback)
|
||||
{
|
||||
final var downloadedTracks=new AtomicInteger();
|
||||
for (var musicTrack:playlist)
|
||||
TrackDownloader.cacheTrack(musicTrack,new Callback()
|
||||
{
|
||||
@Override
|
||||
public void onProgress(int progress) {
|
||||
}
|
||||
public void onProgress(int progress)
|
||||
{ }
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
var currentProgress = downloadedTracks.incrementAndGet();
|
||||
callback.onProgress(currentProgress);
|
||||
}
|
||||
public void onSuccess()
|
||||
{ callback.onProgress(downloadedTracks.incrementAndGet()); }
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
callback.onFailure();
|
||||
}
|
||||
public void onFailure()
|
||||
{ callback.onFailure(); }
|
||||
|
||||
@Override
|
||||
public void onSizeReceived(long size, long header) {
|
||||
}
|
||||
public void onSizeReceived(long size, long header)
|
||||
{ }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.cache.FileCacheImplementation;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -11,36 +11,43 @@ import java.net.URL;
|
||||
public class ThumbnailDownloader {
|
||||
|
||||
public static void downloadThumbnails(MusicTrack track)
|
||||
throws IOException {
|
||||
var obj = track.J();
|
||||
var trackId = LibVKXClient.asId(track);
|
||||
|
||||
if (!obj.has("album")) return;
|
||||
var album = obj.optJSONObject("album");
|
||||
|
||||
if (!album.has("thumb")) return;
|
||||
var thumb = album.optJSONObject("thumb");
|
||||
|
||||
if (thumb.has("sizes")) {
|
||||
var sizes = thumb.optJSONArray("sizes");
|
||||
for (int i = 0; i < sizes.length(); i++) {
|
||||
var size = sizes.optJSONObject(i);
|
||||
var width = size.optInt("width");
|
||||
var src = size.optString("src");
|
||||
downloadThumbnail(src, width, trackId);
|
||||
throws IOException
|
||||
{
|
||||
do
|
||||
{
|
||||
final var json=track.J();
|
||||
final var album=json.optJSONObject("album");
|
||||
if(album==null)break;
|
||||
final var thumb=album.optJSONObject("thumb");
|
||||
if(thumb==null)break;
|
||||
final var sizes=thumb.optJSONArray("sizes");
|
||||
if(sizes==null)
|
||||
{
|
||||
final var names=thumb.names();
|
||||
if(names==null)break;
|
||||
final var trackId=LibVKXClient.asId(track);
|
||||
for(var i=0;i<names.length();++i)
|
||||
{
|
||||
var name=names.optString(i);
|
||||
downloadThumbnail(thumb.optString(name),Integer.parseInt(name.substring(6)),trackId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var names = thumb.names();
|
||||
for (int i = 0; i < names.length(); i++) {
|
||||
var name = names.optString(i);
|
||||
downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId);
|
||||
else
|
||||
{
|
||||
final var trackId=LibVKXClient.asId(track);
|
||||
for(var i=0;i<sizes.length();++i)
|
||||
{
|
||||
var size=sizes.optJSONObject(i);
|
||||
var width=size.optInt("width");
|
||||
var src=size.optString("src");
|
||||
downloadThumbnail(src,width,trackId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}while(false);
|
||||
}
|
||||
|
||||
private static void downloadThumbnail(String url, int res, String trackId)
|
||||
throws IOException {
|
||||
var data = IOUtils.readFully(new URL(url).openStream());
|
||||
IOUtils.writeToFile(FileCacheImplementation.getTrackThumbnail(trackId, res), data);
|
||||
}
|
||||
private static void downloadThumbnail(final String url,final int res,final String trackId)
|
||||
throws IOException
|
||||
{ IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId,res),IOUtils.readFully(new URL(url).openStream())); }
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import android.util.Log;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.concurrent.VTExecutors;
|
||||
import ru.vtosters.lite.music.Callback;
|
||||
import ru.vtosters.lite.music.M3UDownloader;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.FileCacheImplementation;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
|
||||
import java.io.File;
|
||||
@@ -24,7 +23,7 @@ public class TrackDownloader {
|
||||
}
|
||||
|
||||
public static void cacheTrack(MusicTrack track, Callback callback) {
|
||||
if (CacheDatabaseDelegate.isCached(track.y1())) {
|
||||
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) {
|
||||
callback.onSuccess();
|
||||
return;
|
||||
} else if (track.D.isEmpty()) {
|
||||
@@ -33,7 +32,7 @@ public class TrackDownloader {
|
||||
return;
|
||||
}
|
||||
|
||||
var path = FileCacheImplementation.getTrackFolder(LibVKXClient.asId(track)).getAbsolutePath();
|
||||
var path = MusicCacheStorageUtils.getTrackDirById(LibVKXClient.asId(track)).getAbsolutePath();
|
||||
download(track, path, callback, M3UDownloader.getInstance(), true);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package ru.vtosters.lite.music;
|
||||
package ru.vtosters.lite.music.interfaces;
|
||||
|
||||
public interface Callback {
|
||||
void onProgress(int progress);
|
||||
@@ -1,7 +1,6 @@
|
||||
package ru.vtosters.lite.music.interfaces;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.Callback;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import ru.vtosters.lite.concurrent.VTExecutors;
|
||||
import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
import ru.vtosters.lite.downloaders.VideoDownloader;
|
||||
import ru.vtosters.lite.music.LastFMScrobbler;
|
||||
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.proxy.ProxyUtils;
|
||||
import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter;
|
||||
import ru.vtosters.lite.utils.AccountManagerUtils;
|
||||
@@ -108,9 +108,9 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
|
||||
findPreference("lastfm_enabled").setEnabled(false);
|
||||
}
|
||||
|
||||
findPreference("cached_tracks").setSummary(String.format(requireContext().getString(R.string.cached_tracks_counter), CacheDatabaseDelegate.getTrackCount()));
|
||||
findPreference("cached_tracks").setSummary(String.format(requireContext().getString(R.string.cached_tracks_counter),MusicCacheImpl.getTracksCount()));
|
||||
findPreference("cached_tracks").setOnPreferenceClickListener(preference -> {
|
||||
if (CacheDatabaseDelegate.getTrackCount() == 0) {
|
||||
if (MusicCacheImpl.isEmpty()) {
|
||||
AndroidUtils.sendToast(requireContext().getString(R.string.no_cache_error));
|
||||
} else {
|
||||
delcache(requireContext());
|
||||
@@ -123,7 +123,7 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
|
||||
return true;
|
||||
});
|
||||
|
||||
findPreference("invertCachedTracks").setVisible(CacheDatabaseDelegate.hasTracks());
|
||||
findPreference("invertCachedTracks").setVisible(!MusicCacheImpl.isEmpty());
|
||||
|
||||
if (!LibVKXClient.isVkxInstalled()) {
|
||||
findPreference("vkx_sett").setVisible(false);
|
||||
@@ -217,7 +217,7 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.cached_tracks_remove_confirm)
|
||||
.setPositiveButton(R.string.yes,
|
||||
(dialog, which) -> executor.submit(CacheDatabaseDelegate::clear))
|
||||
(dialog, which) -> executor.submit(MusicCacheImpl::clear))
|
||||
.setNeutralButton(R.string.no,
|
||||
(dialog, which) -> dialog.cancel())
|
||||
.show();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package ru.vtosters.lite.ui.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.*;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
@@ -1,40 +1,13 @@
|
||||
package ru.vtosters.lite.utils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class IOUtils {
|
||||
public static int BUFFER_SIZE = 8192;
|
||||
|
||||
public static byte[] decodeStream(InputStream encIs, byte[] aesKey)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
|
||||
CipherInputStream cip = new CipherInputStream(encIs, getCipher(aesKey));
|
||||
return readFully(cip);
|
||||
}
|
||||
|
||||
public static Cipher getCipher(byte[] aesKey)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
Key keySpec = new SecretKeySpec(aesKey, "AES");
|
||||
byte[] iv = new byte[16];
|
||||
Arrays.fill(iv, (byte) 0x0);
|
||||
IvParameterSpec ivps = new IvParameterSpec(iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
|
||||
return cipher;
|
||||
}
|
||||
|
||||
public static InputStream openStream(String url)
|
||||
throws IOException {
|
||||
URLConnection connection = new URL(url).openConnection();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package ru.vtosters.lite.utils.music;
|
||||
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class MusicCacheStorageUtils
|
||||
{
|
||||
static public File getCacheStorageDir()
|
||||
{ return AndroidUtils.getGlobalContext().getExternalFilesDir("vt_tracks"); }
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
static public File getTrackDirById(final String trackId)
|
||||
{
|
||||
final var res=new File(getCacheStorageDir(),trackId);
|
||||
res.mkdirs();
|
||||
return res;
|
||||
}
|
||||
|
||||
static public File getTrackFile(final String trackId)
|
||||
{ return new File(getTrackDirById(trackId),"track.mp3"); }
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
static public File getThumbDirById(final String trackId)
|
||||
{
|
||||
final var res=new File(getTrackDirById(trackId),"thumbs");
|
||||
res.mkdirs();
|
||||
return res;
|
||||
}
|
||||
|
||||
static public File getTrackThumb(final String trackId,final int factor)
|
||||
{ return new File(getThumbDirById(trackId),"thumb_"+factor+".png"); }
|
||||
|
||||
static public void removeTrackDirById(final String trackId)
|
||||
{ IOUtils.deleteRecursive(getTrackDirById(trackId)); }
|
||||
|
||||
static public void clear()
|
||||
{ IOUtils.deleteRecursive(getCacheStorageDir()); }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package ru.vtosters.lite.utils.music;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
|
||||
|
||||
public class VKXUtils
|
||||
{
|
||||
public static boolean isVkxCached(int playlist_id, int owner_id) {
|
||||
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
|
||||
@Override
|
||||
public Boolean run(ILibVkxService service) {
|
||||
try {
|
||||
return service.isPlaylistCached(playlist_id, owner_id);
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean defaultValue() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isVkxCached(String trackId) {
|
||||
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
|
||||
@Override
|
||||
public Boolean run(ILibVkxService service) {
|
||||
try {
|
||||
var splits = trackId.split("_");
|
||||
var id = Integer.parseInt(splits[1]);
|
||||
var owner = Integer.parseInt(splits[0]);
|
||||
return service.isTrackCached(id, owner);
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean defaultValue() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
invoke-static {p1, v1}, Lkotlin/jvm/internal/Intrinsics;->a(Ljava/lang/Object;Ljava/lang/String;)V
|
||||
|
||||
invoke-static/range {p1 .. p1}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->injectIntoExistingCatalog(Lorg/json/JSONObject;)V
|
||||
invoke-static/range {p1 .. p1}, Lru/vtosters/hooks/music/injectors/TracklistInjector;->injectIntoExistingCatalog(Lorg/json/JSONObject;)V
|
||||
|
||||
invoke-virtual {v0, p1}, Lcom/vk/catalog2/core/CatalogParser;->c(Lorg/json/JSONObject;)Lcom/vk/catalog2/core/api/dto/CatalogResponse;
|
||||
|
||||
|
||||
+2
-2
@@ -83,8 +83,8 @@
|
||||
.locals 1
|
||||
|
||||
.line 21
|
||||
# ru.vtosters.lite.music.hook.MusicArtistSelectorHook.navigateToSearch(Context context, String artist)
|
||||
invoke-static {p1, p2}, Lru/vtosters/lite/music/hook/MusicArtistSelectorHook;->navigateToSearch(Landroid/content/Context;Ljava/lang/String;)V
|
||||
# ru.vtosters.hooks.music.MusicArtistSelectorHook.navigateToSearch(Context context, String artist)
|
||||
invoke-static {p1, p2}, Lru/vtosters/hooks/music/MusicArtistSelectorHook;->navigateToSearch(Landroid/content/Context;Ljava/lang/String;)V
|
||||
|
||||
# new-instance v0, Lcom/vk/music/search/MusicSearchFragment$a;
|
||||
#
|
||||
|
||||
@@ -466,7 +466,7 @@
|
||||
:cond_8
|
||||
iget-object v2, v0, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheet;->b:Lcom/vk/dto/music/Playlist;
|
||||
|
||||
invoke-static {v1, v2}, Lru/vtosters/lite/music/hook/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;Lcom/vk/dto/music/Playlist;)Ljava/util/ArrayList;
|
||||
invoke-static {v1, v2}, Lru/vtosters/hooks/music/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;Lcom/vk/dto/music/Playlist;)Ljava/util/ArrayList;
|
||||
|
||||
move-result-object v1
|
||||
|
||||
|
||||
+2
-2
@@ -139,7 +139,7 @@
|
||||
|
||||
move-result p1
|
||||
|
||||
invoke-static {p1, p2}, Lru/vtosters/lite/music/hook/MusicBottomSheetHook;->injectOnClick(ILcom/vk/dto/music/Playlist;)Z
|
||||
invoke-static {p1, p2}, Lru/vtosters/hooks/music/MusicBottomSheetHook;->injectOnClick(ILcom/vk/dto/music/Playlist;)Z
|
||||
|
||||
move-result v3
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
iget-object v0, p0, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener;->e:Lcom/vk/dto/music/Playlist;
|
||||
|
||||
invoke-static/range {v0 .. v0}, Lru/vtosters/lite/music/cache/injectors/PlaylistInjector;->injectDownloadPlaylist(Lcom/vk/dto/music/Playlist;)V
|
||||
invoke-static/range {v0 .. v0}, Lru/vtosters/hooks/music/injectors/PlaylistInjector;->injectDownloadPlaylist(Lcom/vk/dto/music/Playlist;)V
|
||||
|
||||
goto/16 :goto_0
|
||||
|
||||
|
||||
+1
-1
@@ -716,7 +716,7 @@
|
||||
|
||||
move-result-object v3
|
||||
|
||||
invoke-static {p1, v0, v2, v3}, Lru/vtosters/lite/music/hook/MusicBottomSheetHook;->injectOnClick(ILcom/vk/dto/music/MusicTrack;Lcom/vk/music/common/MusicPlaybackLaunchContext;Lcom/vk/dto/music/Playlist;)Z
|
||||
invoke-static {p1, v0, v2, v3}, Lru/vtosters/hooks/music/MusicBottomSheetHook;->injectOnClick(ILcom/vk/dto/music/MusicTrack;Lcom/vk/music/common/MusicPlaybackLaunchContext;Lcom/vk/dto/music/Playlist;)Z
|
||||
|
||||
move-result v3
|
||||
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@
|
||||
|
||||
move-object/from16 v10, p1
|
||||
|
||||
invoke-static {v1, v10}, Lru/vtosters/lite/music/hook/MusicBottomSheetHook;->hookDownloadBTN(Ljava/util/ArrayList;Lcom/vk/dto/music/MusicTrack;)Ljava/util/ArrayList;
|
||||
invoke-static {v1, v10}, Lru/vtosters/hooks/music/MusicBottomSheetHook;->hookDownloadBTN(Ljava/util/ArrayList;Lcom/vk/dto/music/MusicTrack;)Ljava/util/ArrayList;
|
||||
|
||||
move-result-object v1
|
||||
|
||||
@@ -705,7 +705,7 @@
|
||||
|
||||
.line 31
|
||||
:cond_9
|
||||
invoke-static {v3, v1}, Lru/vtosters/lite/music/hook/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;Lcom/vk/dto/music/MusicTrack;)Ljava/util/ArrayList;
|
||||
invoke-static {v3, v1}, Lru/vtosters/hooks/music/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;Lcom/vk/dto/music/MusicTrack;)Ljava/util/ArrayList;
|
||||
|
||||
move-result-object v3
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
.end annotation
|
||||
|
||||
invoke-static {}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->eligibleForOfflineCaching()Z
|
||||
invoke-static {}, Lru/vtosters/hooks/music/injectors/TracklistInjector;->eligibleForOfflineCaching()Z
|
||||
|
||||
move-result v0
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
move-result-object v0
|
||||
|
||||
invoke-static {v0}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->createOfflineRx(Lcom/vk/catalog2/core/CatalogParser;)Lio/reactivex/Observable;
|
||||
invoke-static {v0}, Lru/vtosters/hooks/music/injectors/TracklistInjector;->createOfflineRx(Lcom/vk/catalog2/core/CatalogParser;)Lio/reactivex/Observable;
|
||||
|
||||
move-result-object v0
|
||||
|
||||
|
||||
@@ -1780,13 +1780,13 @@
|
||||
|
||||
.line 90
|
||||
:cond_3
|
||||
invoke-static {}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->eligibleForOfflineCaching()Z
|
||||
invoke-static {}, Lru/vtosters/hooks/music/injectors/TracklistInjector;->eligibleForOfflineCaching()Z
|
||||
|
||||
move-result v0
|
||||
|
||||
if-eqz v0, :cond_4
|
||||
|
||||
invoke-static {p0}, Lru/vtosters/lite/music/cache/injectors/ShuffleInjector;->shuffleTracks(Lcom/vk/music/model/IpcPlayerModelImpl;)V
|
||||
invoke-static {p0}, Lru/vtosters/hooks/music/injectors/ShuffleInjector;->shuffleTracks(Lcom/vk/music/model/IpcPlayerModelImpl;)V
|
||||
|
||||
return-void
|
||||
|
||||
|
||||
@@ -1362,13 +1362,13 @@
|
||||
|
||||
.line 44
|
||||
:cond_0
|
||||
invoke-static {}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->eligibleForOfflineCaching()Z
|
||||
invoke-static {}, Lru/vtosters/hooks/music/injectors/TracklistInjector;->eligibleForOfflineCaching()Z
|
||||
|
||||
move-result v0
|
||||
|
||||
if-eqz v0, :cond_1
|
||||
|
||||
invoke-static {p0}, Lru/vtosters/lite/music/cache/injectors/ShuffleInjector;->shuffleTracks(Lcom/vk/music/model/PlayerModelImpl;)V
|
||||
invoke-static {p0}, Lru/vtosters/hooks/music/injectors/ShuffleInjector;->shuffleTracks(Lcom/vk/music/model/PlayerModelImpl;)V
|
||||
|
||||
return-void
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
|
||||
const/4 v1, 0x1
|
||||
|
||||
invoke-static {p1}, Lru/vtosters/lite/music/cache/injectors/PlaylistInjector;->injectGetPlaylist(Lcom/vk/api/audio/AudioGetPlaylist;)Lio/reactivex/Observable;
|
||||
invoke-static {p1}, Lru/vtosters/hooks/music/injectors/PlaylistInjector;->injectGetPlaylist(Lcom/vk/api/audio/AudioGetPlaylist;)Lio/reactivex/Observable;
|
||||
|
||||
move-result-object v2
|
||||
|
||||
|
||||
@@ -593,7 +593,7 @@
|
||||
|
||||
move-result-object v0
|
||||
|
||||
invoke-static/range {v0 .. v0}, Lru/vtosters/lite/music/cache/injectors/PlaylistInjector;->injectDownloadPlaylist(Lcom/vk/dto/music/Playlist;)V
|
||||
invoke-static/range {v0 .. v0}, Lru/vtosters/hooks/music/injectors/PlaylistInjector;->injectDownloadPlaylist(Lcom/vk/dto/music/Playlist;)V
|
||||
|
||||
return-void
|
||||
|
||||
|
||||
@@ -423,9 +423,9 @@
|
||||
.end annotation
|
||||
.end param
|
||||
|
||||
# ru.vtosters.lite.music.hook.PlaylistFormatterHook -> public static CharSequence titleAndSubtitleMergeFix(Context context, Playlist playlist, @AttrRes int i)
|
||||
# ru.vtosters.hooks.music.PlaylistFormatterHook -> public static CharSequence titleAndSubtitleMergeFix(Context context, Playlist playlist, @AttrRes int i)
|
||||
.line 15
|
||||
invoke-static {p1, p2, p3}, Lru/vtosters/lite/music/hook/PlaylistFormatterHook;->titleAndSubtitleMergeFix(Landroid/content/Context;Lcom/vk/dto/music/Playlist;I)Ljava/lang/CharSequence;
|
||||
invoke-static {p1, p2, p3}, Lru/vtosters/hooks/music/PlaylistFormatterHook;->titleAndSubtitleMergeFix(Landroid/content/Context;Lcom/vk/dto/music/Playlist;I)Ljava/lang/CharSequence;
|
||||
|
||||
move-result-object p1
|
||||
|
||||
@@ -888,7 +888,7 @@
|
||||
|
||||
if-eqz v0, :cond_1
|
||||
|
||||
invoke-static {p1, p2}, Lru/vtosters/lite/music/hook/PlaylistFormatterHook;->getOwnerText(Landroid/content/Context;Lcom/vk/dto/music/Playlist;)Ljava/lang/String;
|
||||
invoke-static {p1, p2}, Lru/vtosters/hooks/music/PlaylistFormatterHook;->getOwnerText(Landroid/content/Context;Lcom/vk/dto/music/Playlist;)Ljava/lang/String;
|
||||
|
||||
move-result-object p1
|
||||
|
||||
|
||||
@@ -112,14 +112,14 @@
|
||||
.method static a(Ljava/lang/String;)Ljava/io/File;
|
||||
.locals 6
|
||||
|
||||
invoke-static {p0}, Lru/vtosters/hooks/MusicCacheFilesHook;->isTrackExist(Ljava/lang/String;)Z
|
||||
invoke-static {p0}, Lru/vtosters/hooks/music/MusicCacheFilesHook;->isTrackExist(Ljava/lang/String;)Z
|
||||
|
||||
move-result v5
|
||||
|
||||
if-eqz v5, :cond_228
|
||||
|
||||
.line 5
|
||||
invoke-static {p0}, Lru/vtosters/hooks/MusicCacheFilesHook;->getTrackFile(Ljava/lang/String;)Ljava/io/File;
|
||||
invoke-static {p0}, Lru/vtosters/hooks/music/MusicCacheFilesHook;->getTrackFile(Ljava/lang/String;)Ljava/io/File;
|
||||
|
||||
move-result-object v0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user