optimize music cache logic

This commit is contained in:
Vologhat
2023-09-24 18:39:12 +03:00
parent 60c98cd998
commit d9f53920e5
54 changed files with 953 additions and 1083 deletions
@@ -10,9 +10,9 @@ import com.vk.music.common.MusicPlaybackLaunchContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.getBoolValue;
import static ru.vtosters.hooks.other.Preferences.isValidSignature; import static ru.vtosters.hooks.other.Preferences.isValidSignature;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
public class LibVKXClient { public class LibVKXClient {
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@@ -80,11 +80,6 @@ public class LibVKXClient {
lambdaplay(list, musicTrack, iLibVkxService, null); lambdaplay(list, musicTrack, iLibVkxService, null);
} }
public static String asId(MusicTrack musicTrack) { public static String asId(MusicTrack musicTrack)
String id = musicTrack.e + "_" + musicTrack.d; { return musicTrack.y1(); }
if (musicTrack.O != null && !musicTrack.O.equals("")) {
id += "_" + musicTrack.O;
}
return id;
}
} }
@@ -6,9 +6,9 @@ import com.vk.api.base.ApiRequest;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.log.L; import com.vk.log.L;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.Genius; import ru.vtosters.lite.music.Genius;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.hooks.other.Preferences;
import java.util.ArrayList; import java.util.ArrayList;
@@ -23,8 +23,8 @@ import com.vtosters.lite.R;
import com.vtosters.lite.ViewUtils; import com.vtosters.lite.ViewUtils;
import com.vtosters.lite.ui.MusicErrorViewHelper; import com.vtosters.lite.ui.MusicErrorViewHelper;
import com.vtosters.lite.ui.holder.RecyclerHolder; import com.vtosters.lite.ui.holder.RecyclerHolder;
import ru.vtosters.lite.music.Genius;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.Genius;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -2,7 +2,7 @@ package ru.vtosters.hooks;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.lite.music.catalog.inject.CatalogJsonInjector; import ru.vtosters.hooks.music.injectors.CatalogJsonInjector;
public class CatalogsHook { public class CatalogsHook {
public static JSONObject injectCatalogGetAudio(JSONObject json) { 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,4 +1,4 @@
package ru.vtosters.lite.music.hook; package ru.vtosters.hooks.music;
import android.content.Context; import android.content.Context;
import com.vk.music.fragment.MusicFragment; import com.vk.music.fragment.MusicFragment;
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.hook; package ru.vtosters.hooks.music;
import android.os.RemoteException; import android.os.RemoteException;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl; 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.vk.music.common.MusicPlaybackLaunchContext;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.music.VKXUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.*; import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.*;
import static ru.vtosters.hooks.other.Preferences.milkshake; 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; import static ru.vtosters.lite.utils.NetworkUtils.isNetworkConnected;
public class MusicBottomSheetHook { 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; if (musicTrack.F1()) return actions;
final var trackId=asId(musicTrack);
var trackid = asId(musicTrack); if (isVkxInstalled())actions.add(getPlayInVKXAction());
if (isIntegrationEnabled())
if (isVkxInstalled()) { {
actions.add(getPlayInVKXAction()); if(VKXUtils.isVkxCached(trackId))actions.add(getRemoveCacheTrackVkxAction());
else if(isNetworkConnected())actions.add(addToCacheTrackVkxAction());
} }
else
if (isIntegrationEnabled()) { {
if (isVkxCached(trackid)) { if(MusicCacheImpl.isCachedTrack(trackId))actions.add(getRemoveCacheTrackAction());
actions.add(getRemoveCacheTrackVkxAction()); else if(isNetworkConnected())actions.add(addToCacheTrackAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackVkxAction());
}
} else {
if (isCached(trackid)) {
actions.add(getRemoveCacheTrackAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackAction());
}
} }
if (isNetworkConnected())actions.add(downloadAsMp3Action());
if (isNetworkConnected()) {
actions.add(downloadAsMp3Action());
}
return actions; return actions;
} }
@@ -54,9 +42,7 @@ public class MusicBottomSheetHook {
public static ArrayList<MusicAction> hookDownloadBTN(ArrayList<MusicAction> actions, MusicTrack musicTrack) { public static ArrayList<MusicAction> hookDownloadBTN(ArrayList<MusicAction> actions, MusicTrack musicTrack) {
if (musicTrack.F1()) return actions; if (musicTrack.F1()) return actions;
var trackid = asId(musicTrack); if (MusicCacheImpl.isCachedTrack(musicTrack.y1())) {
if (isCached(trackid)) {
actions.add(new MusicAction( actions.add(new MusicAction(
R.id.music_action_toggle_download, R.id.music_action_toggle_download,
milkshake() ? R.drawable.ic_delete_outline_28 : R.drawable.ic_delete_24, 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 (isVkxInstalled()) actions.add(getPlayInVKXAction());
if (isIntegrationEnabled()) { if (isIntegrationEnabled()) {
if (isVkxCached(playlist.a, playlist.b)) { if (VKXUtils.isVkxCached(playlist.a, playlist.b)) {
actions.add(getRemoveCacheTrackVkxAction()); actions.add(getRemoveCacheTrackVkxAction());
} else if (isNetworkConnected()) { } else if (isNetworkConnected()) {
actions.add(addToCacheTrackVkxAction()); actions.add(addToCacheTrackVkxAction());
@@ -124,7 +110,7 @@ public class MusicBottomSheetHook {
} }
if (actionId == R.id.remove_from_cache) { if (actionId == R.id.remove_from_cache) {
CacheDatabaseDelegate.removeTrackFromCache(asId(track)); MusicCacheImpl.removeTrack(asId(track));
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache")); AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
return true; return true;
} else if (actionId == R.id.add_to_cache) { } 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,4 +1,4 @@
package ru.vtosters.lite.music.hook; package ru.vtosters.hooks.music;
import android.content.Context; import android.content.Context;
import androidx.annotation.AttrRes; import androidx.annotation.AttrRes;
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.catalog.inject; package ru.vtosters.hooks.music.injectors;
import android.util.Log; import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
@@ -12,7 +12,7 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.di.singleton.VtOkHttpClient; 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.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
@@ -48,7 +48,7 @@ public class CatalogJsonInjector {
setDefaultAudioPage(oldItems, catalog); 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"); var noPlaylists = !json.has("playlists");
if (noPlaylists) { if (noPlaylists) {
@@ -91,7 +91,7 @@ public class CatalogJsonInjector {
fixDailyMix(blocks); 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 return; // early return if not users catalog or no tracks or integration enabled
} }
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.cache.injectors; package ru.vtosters.hooks.music.injectors;
import android.os.RemoteException; import android.os.RemoteException;
import android.text.TextUtils; import android.text.TextUtils;
@@ -12,7 +12,7 @@ import io.reactivex.ObservableOnSubscribe;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.lite.downloaders.AudioDownloader; 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.PlaylistHelper;
import ru.vtosters.lite.music.cache.helpers.TracklistHelper; import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
@@ -25,13 +25,11 @@ public class PlaylistInjector {
public final static String CHANNEL_NAME = "VTCH"; public final static String CHANNEL_NAME = "VTCH";
private static final ExecutorService executor = Executors.newCachedThreadPool(); private static final ExecutorService executor = Executors.newCachedThreadPool();
public static void injectDownloadPlaylist(Playlist playlist) { public static void injectDownloadPlaylist(Playlist playlist)
executor.submit(() -> AudioDownloader.cachePlaylist(playlist)); { executor.submit(() -> AudioDownloader.cachePlaylist(playlist)); }
}
public static boolean eligibleForOfflineCaching() { public static boolean eligibleForOfflineCaching()
return CacheDatabaseDelegate.hasTracks(); { return !MusicCacheImpl.isEmpty(); }
}
public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) { public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) {
try { try {
@@ -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.dto.music.MusicTrack;
import com.vk.music.model.IpcPlayerModelImpl; import com.vk.music.model.IpcPlayerModelImpl;
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.cache.injectors; package ru.vtosters.hooks.music.injectors;
import android.util.Log; import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
@@ -11,7 +11,7 @@ import io.reactivex.ObservableOnSubscribe;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import 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.PlaylistHelper;
import ru.vtosters.lite.music.cache.helpers.TracklistHelper; import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
import ru.vtosters.lite.utils.AccountManagerUtils; import ru.vtosters.lite.utils.AccountManagerUtils;
@@ -56,7 +56,7 @@ public class TracklistInjector {
} }
}); });
} else { } else {
if (!CacheDatabaseDelegate.hasTracks()) { if (MusicCacheImpl.isEmpty()) {
observableEmitter.b(parser.c(getEmptyCatalog())); observableEmitter.b(parser.c(getEmptyCatalog()));
} else { } else {
observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks()))); observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks())));
@@ -135,7 +135,7 @@ public class TracklistInjector {
button.put("action", action); button.put("action", action);
button.put("block_id", "shuffle"); 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_layout_name", "list");
button.put("ref_data_type", "music_audios"); button.put("ref_data_type", "music_audios");
@@ -158,7 +158,7 @@ public class TracklistInjector {
JSONArray blocks = firstSection.getJSONArray("blocks"); 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); boolean noPlaylists = hasNoPlaylists(blocks);
if (noPlaylists) { if (noPlaylists) {
@@ -1,24 +1,24 @@
package ru.vtosters.lite.downloaders; package ru.vtosters.lite.downloaders;
import android.os.Environment; import android.os.Environment;
import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.core.util.ToastUtils; import com.vk.core.util.ToastUtils;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist; import com.vk.dto.music.Playlist;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import ru.vtosters.hooks.MusicCacheFilesHook; import ru.vtosters.hooks.music.MusicCacheFilesHook;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.callback.MusicCallbackBuilder; import ru.vtosters.lite.music.callback.MusicCallbackBuilder;
import ru.vtosters.lite.music.converter.playlist.PlaylistConverter; import ru.vtosters.lite.music.converter.playlist.PlaylistConverter;
import ru.vtosters.lite.music.downloader.AudioGet; import ru.vtosters.lite.music.downloader.*;
import ru.vtosters.lite.music.downloader.PlaylistDownloader;
import ru.vtosters.lite.music.downloader.TrackDownloader;
import ru.vtosters.lite.music.notification.MusicNotificationBuilder; import ru.vtosters.lite.music.notification.MusicNotificationBuilder;
import ru.vtosters.lite.utils.AccountManagerUtils; import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils; import ru.vtosters.lite.utils.IOUtils;
import java.io.File; import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -33,7 +33,8 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
public class AudioDownloader { public class AudioDownloader {
public static final ExecutorService executor = Executors.newCachedThreadPool(); 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 tracks = PlaylistConverter.getPlaylist(playlist);
var playlistName = IOUtils.getValidFileName(playlist.g); var playlistName = IOUtils.getValidFileName(playlist.g);
@@ -53,15 +54,14 @@ public class AudioDownloader {
} }
public static void downloadAudio(MusicTrack track) { public static void downloadAudio(MusicTrack track)
downloadM3U8(track, false); { downloadM3U8(track,false); }
}
public static void cacheTrack(MusicTrack track) { public static void cacheTrack(MusicTrack track) {
var trackId = track.y1(); var trackId = LibVKXClient.asId(track);
if (CacheDatabaseDelegate.isCached(trackId)) { if (MusicCacheImpl.isCachedTrack(trackId)) {
CacheDatabaseDelegate.removeTrackFromCache(LibVKXClient.asId(track)); MusicCacheImpl.removeTrack(trackId);
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache")); AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
return; return;
} }
@@ -14,7 +14,7 @@ import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.di.singleton.VtOkHttpClient; import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.downloaders.AudioDownloader; 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 ru.vtosters.lite.utils.AndroidUtils;
import java.io.IOException; import java.io.IOException;
@@ -47,7 +47,7 @@ public class LastFMScrobbler {
return; return;
} }
if (Preferences.autocache() && !CacheDatabaseDelegate.isCached(uid)) { if (Preferences.autocache() && !MusicCacheImpl.isCachedTrack(uid)) {
if (LibVKXClient.isIntegrationEnabled()) { if (LibVKXClient.isIntegrationEnabled()) {
LibVKXClient.getInstance().runOnService(service -> { LibVKXClient.getInstance().runOnService(service -> {
try { 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; package ru.vtosters.lite.music.cache.helpers;
import android.net.Uri;
import com.vk.dto.music.Playlist; import com.vk.dto.music.Playlist;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.FileCacheImplementation; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
@@ -44,8 +45,8 @@ public class PlaylistHelper {
if (!photo.isEmpty()) if (!photo.isEmpty())
jPhoto.put("height", 600) jPhoto.put("height", 600)
.put("width", 600) .put("width", 600)
.putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300))) .putOpt("photo_300",Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId,300)).toString())
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600))); .putOpt("photo_600",Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId,600)).toString());
var obj = new JSONObject() var obj = new JSONObject()
.put("id", -2) .put("id", -2)
.put("owner_id", albumId) .put("owner_id", albumId)
@@ -68,7 +69,7 @@ public class PlaylistHelper {
public static List<Playlist> getAlbumPlaylists() { public static List<Playlist> getAlbumPlaylists() {
List<Playlist> list = new ArrayList<>(); List<Playlist> list = new ArrayList<>();
for (var track : CacheDatabaseDelegate.getTracksAsPlaylist()) { for (var track : MusicCacheImpl.getPlaylist()) {
var albumLink = track.I; var albumLink = track.I;
var thumb = albumLink.u1(); var thumb = albumLink.u1();
list.add(createAlbum( list.add(createAlbum(
@@ -7,8 +7,8 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences; import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.FileCacheImplementation; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File; import java.io.File;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@@ -22,7 +22,7 @@ public class TracklistHelper {
} }
public static List<MusicTrack> getTracks() { public static List<MusicTrack> getTracks() {
List<MusicTrack> tracks = CacheDatabaseDelegate.getTracks(); List<MusicTrack> tracks = MusicCacheImpl.getAllTracks();
boolean doNotinvertOrder = Preferences.getBoolValue("invertCachedTracks", false); boolean doNotinvertOrder = Preferences.getBoolValue("invertCachedTracks", false);
@@ -35,11 +35,11 @@ public class TracklistHelper {
public static List<MusicTrack> getTracks(String id) { public static List<MusicTrack> getTracks(String id) {
return CacheDatabaseDelegate.getTracksByAlbum(id); return MusicCacheImpl.getAlbumById(id);
} }
public static List<MusicTrack> getTracksOne(String id) { public static List<MusicTrack> getTracksOne(String id) {
return CacheDatabaseDelegate.getTracksByAlbumOne(id); return MusicCacheImpl.getFirstAlbumTrack(id);
} }
public static JSONArray tracksToIds(List<MusicTrack> tracks) { public static JSONArray tracksToIds(List<MusicTrack> tracks) {
@@ -56,7 +56,7 @@ public class TracklistHelper {
var arr = new JSONArray(); var arr = new JSONArray();
for (MusicTrack track : tracks) { for (MusicTrack track : tracks) {
var json = track.J(); var json = track.J();
var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track)); var folder = MusicCacheStorageUtils.getThumbDirById(LibVKXClient.asId(track));
try { try {
addCachedThumbnails(json, folder); addCachedThumbnails(json, folder);
} catch (JSONException | MalformedURLException e) { } catch (JSONException | MalformedURLException e) {
@@ -71,7 +71,7 @@ public class TracklistHelper {
List<MusicTrack> tracks = new ArrayList<>(); List<MusicTrack> tracks = new ArrayList<>();
for (MusicTrack track : list) { for (MusicTrack track : list) {
var json = track.J(); var json = track.J();
var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track)); var folder = MusicCacheStorageUtils.getThumbDirById(LibVKXClient.asId(track));
try { try {
addCachedThumbnails(json, folder); addCachedThumbnails(json, folder);
} catch (JSONException | MalformedURLException e) { } catch (JSONException | MalformedURLException e) {
@@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels; 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; import ru.vtosters.lite.utils.AndroidUtils;
public class MusicCallbackBuilder { public class MusicCallbackBuilder {
@@ -26,7 +26,8 @@ import static ru.vtosters.lite.proxy.ProxyUtils.getApi;
* stuff for converting playlists DTO * stuff for converting playlists DTO
*/ */
public class PlaylistConverter { 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); var tracks = requestPlaylist(0, oldPlaylist.b, oldPlaylist.Q, oldPlaylist.a);
return tracks.stream().filter(track -> !track.D.isEmpty()).collect(Collectors.toList()); return tracks.stream().filter(track -> !track.D.isEmpty()).collect(Collectors.toList());
} }
@@ -1,23 +1,21 @@
package ru.vtosters.lite.music.converter.ts; package ru.vtosters.lite.music.converter.ts;
import android.util.Log;
import com.arthenica.ffmpegkit.FFmpegKit; import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.ReturnCode; import com.arthenica.ffmpegkit.ReturnCode;
import com.vk.core.util.ToastUtils; import com.vk.core.util.ToastUtils;
import com.vk.dto.music.Artist; import com.vk.dto.music.Artist;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import com.vtosters.lite.R; import com.vtosters.lite.R;
import ru.vtosters.lite.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils; import ru.vtosters.lite.utils.IOUtils;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class FFMpeg { public class FFMpeg
private static final int BUFFER_SIZE = 2048; {
/** /**
* @param in Merged .ts file via {@link ru.vtosters.lite.music.converter.ts.TSMerger} * @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 * @param track Track to be converted
* @return True if success, else false * @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 // convert .ts file to .mp3 file via ffmpeg
var sb = new StringBuilder(); final var cmd=" -y -i \""+in.getAbsolutePath()+'"'
sb.append("-y -i \"").append(in.getAbsolutePath()).append("\""); +" -map 0 -dn"
sb.append(" -map 0 -dn "); +" -loglevel error"
//sb.append(" -loglevel error"); +" -hide_banner"
//sb.append(" -hide_banner"); +" -write_id3v2 1"
sb.append(" -write_id3v2 1"); // +" -metadata title=\""+IOUtils.getValidFileName(M3UDownloader.getTitle(track))+'"'
// +" -metadata artist=\""+normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", ")))+'"'
String title = null; // +" -metadata album=\""+normalizeMetadata(track.I.getTitle())+'"'
+" -c copy \""+out+'"';
if (track.f != null) { final var ses=FFmpegKit.execute(cmd);
title = track.f; if(ses.getReturnCode().isValueCancel())throw new RuntimeException(ses.getLogsAsString());
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;
} }
private static void copyAssets() { static String normalizeMetadata(String in)
try { { return in.replace('"','‟'); }
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();
}
}
private static void checkFFMpegLibs() { // private static void copyAssets()
var lib = new File(AndroidUtils.getGlobalContext().getFilesDir(), "libffmpegkit_abidetect.so"); // throws IOException
if (!lib.exists()) { // {
ToastUtils.a(R.string.ffmpegkit_copy); // try
copyAssets(); // {
} // 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; package ru.vtosters.lite.music.converter.ts;
import android.os.Build;
import ru.vtosters.lite.utils.IOUtils; import ru.vtosters.lite.utils.IOUtils;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Pattern;
public class TSMerger { public class TSMerger {
/** /**
@@ -17,58 +16,23 @@ public class TSMerger {
* @param in Directory with .ts files * @param in Directory with .ts files
* @return True if success, else false * @return True if success, else false
*/ */
public static boolean merge(File in, File out) { public static void merge(final File in,final File out)
var tsesList = in.listFiles(); throws Throwable
{
if (tsesList == null) { var tsFiles=in.listFiles((dir,name) -> name.matches(".*\\d+\\.ts"));
return false; 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();
var mergedByteArrayStream = new ByteArrayOutputStream(); for(var f:tsFiles)bis.write(IOUtils.readFully(f));
bis.close();
sortTsesList(tsesList); IOUtils.writeToFile(out,bis.toByteArray());
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);
} }
private static void sortTsesList(File[] tsesList) { static int getFileIndex(final File f)
Arrays.sort(tsesList, (ts1, ts2) -> { {
String ts1Name = ts1.getName(); final var p=Pattern.compile(".*(\\d+)\\.ts");
String ts2Name = ts2.getName(); final var m=p.matcher(f.getName());
int i1 = Integer.parseInt(ts1Name.substring(0, ts1Name.indexOf('.', 1))); if(m.find())return Integer.parseInt(m.group(1));
int i2 = Integer.parseInt(ts2Name.substring(0, ts2Name.indexOf('.', 1))); throw new NullPointerException("Filename has no index"+f.getName());
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;
} }
} }
@@ -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();
}
}
@@ -70,7 +70,7 @@
// } // }
// //
// @Override // @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 android.util.Log;
import com.vk.dto.music.MusicTrack; 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.io.File;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
public class PlaylistDownloader { public class PlaylistDownloader
public static void downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) { {
File outDir = new File(path); 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.exists())
if (outDir.mkdirs()) if(outDir.mkdirs())Log.v("PlaylistDownloader","Directory created");
Log.v("PlaylistDownloader", "Directory created"); else Log.e("PlaylistDownloader","Directory creation failed");
else final var downloadedTracks=new AtomicInteger();
Log.e("PlaylistDownloader", "Directory creation failed"); for (var musicTrack:playlist)
TrackDownloader.downloadTrack(musicTrack, path, new Callback()
var downloadedTracks = new AtomicInteger(0); {
for (MusicTrack musicTrack : playlist) {
TrackDownloader.downloadTrack(musicTrack, path, new Callback() {
@Override @Override
public void onProgress(int progress) { public void onProgress(int progress)
} { }
@Override @Override
public void onSuccess() { public void onSuccess()
var currentProgress = downloadedTracks.incrementAndGet(); { callback.onProgress(downloadedTracks.incrementAndGet()); }
callback.onProgress(currentProgress);
}
@Override @Override
public void onFailure() { public void onFailure() {
@@ -37,36 +39,32 @@ public class PlaylistDownloader {
} }
@Override @Override
public void onSizeReceived(long size, long header) { public void onSizeReceived(long size, long header)
} { }
}); });
}
} }
public static void cachePlaylist(List<MusicTrack> playlist, Callback callback) { static public void cachePlaylist(final List< MusicTrack > playlist,final Callback callback)
var downloadedTracks = new AtomicInteger(0); {
final var downloadedTracks=new AtomicInteger();
for (MusicTrack musicTrack : playlist) { for (var musicTrack:playlist)
TrackDownloader.cacheTrack(musicTrack, new Callback() { TrackDownloader.cacheTrack(musicTrack,new Callback()
{
@Override @Override
public void onProgress(int progress) { public void onProgress(int progress)
} { }
@Override @Override
public void onSuccess() { public void onSuccess()
var currentProgress = downloadedTracks.incrementAndGet(); { callback.onProgress(downloadedTracks.incrementAndGet()); }
callback.onProgress(currentProgress);
}
@Override @Override
public void onFailure() { public void onFailure()
callback.onFailure(); { callback.onFailure(); }
}
@Override @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 bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack; 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 ru.vtosters.lite.utils.IOUtils;
import java.io.IOException; import java.io.IOException;
@@ -11,36 +11,43 @@ import java.net.URL;
public class ThumbnailDownloader { public class ThumbnailDownloader {
public static void downloadThumbnails(MusicTrack track) public static void downloadThumbnails(MusicTrack track)
throws IOException { throws IOException
var obj = track.J(); {
var trackId = LibVKXClient.asId(track); do
{
if (!obj.has("album")) return; final var json=track.J();
var album = obj.optJSONObject("album"); final var album=json.optJSONObject("album");
if(album==null)break;
if (!album.has("thumb")) return; final var thumb=album.optJSONObject("thumb");
var thumb = album.optJSONObject("thumb"); if(thumb==null)break;
final var sizes=thumb.optJSONArray("sizes");
if (thumb.has("sizes")) { if(sizes==null)
var sizes = thumb.optJSONArray("sizes"); {
for (int i = 0; i < sizes.length(); i++) { final var names=thumb.names();
var size = sizes.optJSONObject(i); if(names==null)break;
var width = size.optInt("width"); final var trackId=LibVKXClient.asId(track);
var src = size.optString("src"); for(var i=0;i<names.length();++i)
downloadThumbnail(src, width, trackId); {
var name=names.optString(i);
downloadThumbnail(thumb.optString(name),Integer.parseInt(name.substring(6)),trackId);
}
} }
} else { else
var names = thumb.names(); {
for (int i = 0; i < names.length(); i++) { final var trackId=LibVKXClient.asId(track);
var name = names.optString(i); for(var i=0;i<sizes.length();++i)
downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId); {
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) private static void downloadThumbnail(final String url,final int res,final String trackId)
throws IOException { throws IOException
var data = IOUtils.readFully(new URL(url).openStream()); { IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId,res),IOUtils.readFully(new URL(url).openStream())); }
IOUtils.writeToFile(FileCacheImplementation.getTrackThumbnail(trackId, res), data);
}
} }
@@ -4,10 +4,9 @@ import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.concurrent.VTExecutors; import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.music.Callback; import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.M3UDownloader; import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import ru.vtosters.lite.music.interfaces.ITrackDownloader; import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import java.io.File; import java.io.File;
@@ -24,7 +23,7 @@ public class TrackDownloader {
} }
public static void cacheTrack(MusicTrack track, Callback callback) { public static void cacheTrack(MusicTrack track, Callback callback) {
if (CacheDatabaseDelegate.isCached(track.y1())) { if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) {
callback.onSuccess(); callback.onSuccess();
return; return;
} else if (track.D.isEmpty()) { } else if (track.D.isEmpty()) {
@@ -33,7 +32,7 @@ public class TrackDownloader {
return; return;
} }
var path = FileCacheImplementation.getTrackFolder(LibVKXClient.asId(track)).getAbsolutePath(); var path = MusicCacheStorageUtils.getTrackDirById(LibVKXClient.asId(track)).getAbsolutePath();
download(track, path, callback, M3UDownloader.getInstance(), true); download(track, path, callback, M3UDownloader.getInstance(), true);
} }
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music; package ru.vtosters.lite.music.interfaces;
public interface Callback { public interface Callback {
void onProgress(int progress); void onProgress(int progress);
@@ -1,7 +1,6 @@
package ru.vtosters.lite.music.interfaces; package ru.vtosters.lite.music.interfaces;
import com.vk.dto.music.MusicTrack; import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.Callback;
import java.io.File; 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.AudioDownloader;
import ru.vtosters.lite.downloaders.VideoDownloader; import ru.vtosters.lite.downloaders.VideoDownloader;
import ru.vtosters.lite.music.LastFMScrobbler; 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.proxy.ProxyUtils;
import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter; import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter;
import ru.vtosters.lite.utils.AccountManagerUtils; import ru.vtosters.lite.utils.AccountManagerUtils;
@@ -108,9 +108,9 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
findPreference("lastfm_enabled").setEnabled(false); 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 -> { findPreference("cached_tracks").setOnPreferenceClickListener(preference -> {
if (CacheDatabaseDelegate.getTrackCount() == 0) { if (MusicCacheImpl.isEmpty()) {
AndroidUtils.sendToast(requireContext().getString(R.string.no_cache_error)); AndroidUtils.sendToast(requireContext().getString(R.string.no_cache_error));
} else { } else {
delcache(requireContext()); delcache(requireContext());
@@ -123,7 +123,7 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
return true; return true;
}); });
findPreference("invertCachedTracks").setVisible(CacheDatabaseDelegate.hasTracks()); findPreference("invertCachedTracks").setVisible(!MusicCacheImpl.isEmpty());
if (!LibVKXClient.isVkxInstalled()) { if (!LibVKXClient.isVkxInstalled()) {
findPreference("vkx_sett").setVisible(false); findPreference("vkx_sett").setVisible(false);
@@ -217,7 +217,7 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.cached_tracks_remove_confirm) .setMessage(R.string.cached_tracks_remove_confirm)
.setPositiveButton(R.string.yes, .setPositiveButton(R.string.yes,
(dialog, which) -> executor.submit(CacheDatabaseDelegate::clear)) (dialog, which) -> executor.submit(MusicCacheImpl::clear))
.setNeutralButton(R.string.no, .setNeutralButton(R.string.no,
(dialog, which) -> dialog.cancel()) (dialog, which) -> dialog.cancel())
.show(); .show();
@@ -1,7 +1,10 @@
package ru.vtosters.lite.ui.fragments; package ru.vtosters.lite.ui.fragments;
import android.annotation.SuppressLint; 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.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@@ -1,40 +1,13 @@
package ru.vtosters.lite.utils; 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.io.*;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.charset.StandardCharsets; 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 class IOUtils {
public static int BUFFER_SIZE = 8192; 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) public static InputStream openStream(String url)
throws IOException { throws IOException {
URLConnection connection = new URL(url).openConnection(); 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 {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; invoke-virtual {v0, p1}, Lcom/vk/catalog2/core/CatalogParser;->c(Lorg/json/JSONObject;)Lcom/vk/catalog2/core/api/dto/CatalogResponse;
@@ -83,8 +83,8 @@
.locals 1 .locals 1
.line 21 .line 21
# ru.vtosters.lite.music.hook.MusicArtistSelectorHook.navigateToSearch(Context context, String artist) # ru.vtosters.hooks.music.MusicArtistSelectorHook.navigateToSearch(Context context, String artist)
invoke-static {p1, p2}, Lru/vtosters/lite/music/hook/MusicArtistSelectorHook;->navigateToSearch(Landroid/content/Context;Ljava/lang/String;)V 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; # new-instance v0, Lcom/vk/music/search/MusicSearchFragment$a;
# #
@@ -466,7 +466,7 @@
:cond_8 :cond_8
iget-object v2, v0, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheet;->b:Lcom/vk/dto/music/Playlist; 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 move-result-object v1
@@ -139,7 +139,7 @@
move-result p1 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 move-result v3
@@ -162,7 +162,7 @@
iget-object v0, p0, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener;->e:Lcom/vk/dto/music/Playlist; 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 goto/16 :goto_0
@@ -716,7 +716,7 @@
move-result-object v3 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 move-result v3
@@ -111,7 +111,7 @@
move-object/from16 v10, p1 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 move-result-object v1
@@ -705,7 +705,7 @@
.line 31 .line 31
:cond_9 :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 move-result-object v3
@@ -65,7 +65,7 @@
} }
.end annotation .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 move-result v0
@@ -75,7 +75,7 @@
move-result-object v0 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 move-result-object v0
@@ -1780,13 +1780,13 @@
.line 90 .line 90
:cond_3 :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 move-result v0
if-eqz v0, :cond_4 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 return-void
@@ -1362,13 +1362,13 @@
.line 44 .line 44
:cond_0 :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 move-result v0
if-eqz v0, :cond_1 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 return-void
@@ -239,7 +239,7 @@
const/4 v1, 0x1 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 move-result-object v2
@@ -593,7 +593,7 @@
move-result-object v0 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 return-void
@@ -423,9 +423,9 @@
.end annotation .end annotation
.end param .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 .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 move-result-object p1
@@ -888,7 +888,7 @@
if-eqz v0, :cond_1 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 move-result-object p1
@@ -112,14 +112,14 @@
.method static a(Ljava/lang/String;)Ljava/io/File; .method static a(Ljava/lang/String;)Ljava/io/File;
.locals 6 .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 move-result v5
if-eqz v5, :cond_228 if-eqz v5, :cond_228
.line 5 .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 move-result-object v0