Merge remote-tracking branch 'sunmisc/main' #1554

This commit is contained in:
aliveoutside
2024-03-18 03:17:30 +03:00
23 changed files with 431 additions and 524 deletions
@@ -18,15 +18,12 @@ import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PlaylistInjector {
public final static String CHANNEL_NAME = "VTCH";
private static final ExecutorService executor = Executors.newCachedThreadPool();
public static void injectDownloadPlaylist(Playlist playlist) {
executor.submit(() -> AudioDownloader.cachePlaylist(playlist));
AudioDownloader.cachePlaylist(playlist);
}
public static boolean eligibleForOfflineCaching() {
@@ -1,10 +1,11 @@
package ru.vtosters.lite.concurrent;
import com.vk.core.concurrent.VkExecutors;
import io.reactivex.Scheduler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import io.reactivex.Scheduler;
public class VTExecutors {
public static ExecutorService getMusicDownloadExecutor() {
@@ -32,7 +33,21 @@ public class VTExecutors {
}
private static class LazyHolder {
public static final ExecutorService INSTANCE = Executors.newSingleThreadExecutor();
private static final int MIN_PARALLELISM = 1;
private static final int MAX_PARALLELISM = 7;
private static final int PARALLELISM = clamp(
(Runtime.getRuntime().availableProcessors() >> 1) - 1
);
private static final ExecutorService INSTANCE =
new ForkJoinPool(PARALLELISM,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
(t, e) -> {},
/* FIFO */ true);
private static int clamp(int value) {
return Math.min(LazyHolder.MAX_PARALLELISM, Math.max(value, LazyHolder.MIN_PARALLELISM));
}
}
}
}
@@ -6,6 +6,8 @@ import com.vk.core.util.ToastUtils;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import com.vtosters.lite.R;
import java8.util.concurrent.CompletableFuture;
import ru.vtosters.hooks.music.MusicCacheFilesHook;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
@@ -20,8 +22,7 @@ import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.Objects;
import static ru.vtosters.lite.utils.AndroidUtils.getString;
@@ -32,10 +33,9 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
*/
public class AudioDownloader {
public static final ExecutorService executor = Executors.newCachedThreadPool();
public static final String dlpath = Preferences.getBoolValue("dldir", false) ? Environment.DIRECTORY_DOWNLOADS : Environment.DIRECTORY_MUSIC;
public static void downloadPlaylist(Playlist playlist) {
public static CompletableFuture<Void> downloadPlaylist(Playlist playlist) {
var tracks = PlaylistConverter.getPlaylist(playlist);
var playlistName = IOUtils.getValidFileName(playlist.g);
@@ -46,7 +46,7 @@ public class AudioDownloader {
var notificationId = playlistName.hashCode();
var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlistName, notificationId);
PlaylistDownloader.downloadPlaylist(
return PlaylistDownloader.downloadPlaylist(
tracks,
IOUtils.getValidFileName(playlist.g),
downloadPath,
@@ -65,22 +65,21 @@ public class AudioDownloader {
if (MusicCacheImpl.isCachedTrack(trackId)) {
MusicCacheImpl.removeTrack(trackId);
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
return;
}
} else {
var trackFile = MusicCacheFilesHook.getTrackFile(trackId);
if (!trackFile.exists())
trackFile.getParentFile().mkdirs();
executor.submit(() -> downloadM3U8(track, true));
downloadM3U8(track, true);
}
}
public static void cachePlaylist(Playlist playlist) {
public static CompletableFuture<Void> cachePlaylist(Playlist playlist) {
var tracks = PlaylistConverter.getPlaylist(playlist);
var notificationId = playlist.g.hashCode();
var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlist.g, notificationId);
PlaylistDownloader.cachePlaylist(
return PlaylistDownloader.cachePlaylist(
tracks,
MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId)
);
@@ -117,21 +116,21 @@ public class AudioDownloader {
);
}
private static void downloadM3U8(MusicTrack track, boolean cache) {
if (track.D == null) {
private static void
downloadM3U8(MusicTrack track, boolean cache) {
Objects.requireNonNull(track.D, () -> {
ToastUtils.a(getString(R.string.link_audio_error));
return;
}
return "link must not be null";
});
var musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath();
var tempId = track.d;
var downloadPath = musicPath + File.separator;
var notification = MusicNotificationBuilder.buildDownloadNotification(track, tempId);
if (cache) {
TrackDownloader.cacheTrack(track, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification));
} else {
TrackDownloader.downloadTrack(track, downloadPath, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification));
}
var cb = MusicCallbackBuilder.buildOneTrackCallback(tempId, notification);
if (cache)
TrackDownloader.cacheTrack(track, cb);
else
TrackDownloader.downloadTrack(track, downloadPath, cb);
}
}
@@ -2,22 +2,25 @@ package ru.vtosters.lite.music;
import android.text.TextUtils;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import com.vtosters.lite.R;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.sponsorpost.utils.GzipDecompressor;
import java.io.IOException;
import java.net.URLEncoder;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils;
import ru.vtosters.sponsorpost.utils.GzipDecompressor;
public class Genius {
private static final String KEY = "Bearer ZTejoT_ojOEasIkT9WrMBhBQOz6eYKK5QULCMECmOhvwqjRZ6WbpamFe3geHnvp3";
private static final String URL = "https://api.genius.com";
@@ -25,7 +28,7 @@ public class Genius {
public static String getTextMusic(MusicTrack musictrack) {
String uid = musictrack.y1();
String artist = removeExtras(M3UDownloader.getArtists(musictrack));
String artist = removeExtras(MusicTrackUtils.getArtists(musictrack));
String title = removeExtras(musictrack.f);
int duration = musictrack.h;
@@ -3,19 +3,14 @@ package ru.vtosters.lite.music;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack;
import com.vtosters.lite.R;
import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -24,6 +19,20 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils;
public class LastFMScrobbler {
private static final String KEY = "5965d63402414776c54c266db0211746";
private static final String KEY_SECRET = "d982180eed13275bb948e41cf225f88f";
@@ -33,7 +42,7 @@ public class LastFMScrobbler {
public static void grabMusicTrack(MusicTrack musictrack) {
var uid = musictrack.y1();
var artist = M3UDownloader.getArtists(musictrack);
var artist = MusicTrackUtils.getArtists(musictrack);
var title = musictrack.f;
var duration = musictrack.h;
var track_id = musictrack.d;
@@ -1,17 +1,17 @@
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;
import ru.vtosters.lite.music.cache.db.MusicCacheDb;
public class MusicCacheDbDelegate {
static synchronized private MusicCacheDb connectToDb(Context context) {
synchronized (MusicCacheDb.class) {
private static MusicCacheDb connectToDb(Context context) {
return new MusicCacheDb(context);
}
}
public static void addTrack(Context context,
String trackId,
@@ -2,15 +2,17 @@ package ru.vtosters.lite.music.cache;
import android.os.RemoteException;
import android.text.TextUtils;
import com.vk.dto.music.MusicTrack;
import java.util.List;
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.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.util.List;
import ru.vtosters.lite.utils.music.MusicTrackUtils;
public class MusicCacheImpl {
public static void addTrack(MusicTrack track) {
@@ -20,7 +22,7 @@ public class MusicCacheImpl {
track.I != null ? track.I.getId() + "" : "-1",
track.f,
!TextUtils.isEmpty(track.g) ? track.g : "",
M3UDownloader.getArtists(track),
MusicTrackUtils.getArtists(track),
track.I != null ? track.I.getTitle() : "",
track.K,
track.h,
@@ -8,14 +8,14 @@ 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.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File;
import java.lang.annotation.Retention;
@@ -24,6 +24,10 @@ import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils;
/**
* <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>
@@ -117,7 +121,7 @@ public class MusicCacheDb extends SQLiteOpenHelper implements AutoCloseable { //
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, M3UDownloader.getArtists(track));
vals.put(Constants.COLUMN_ARTIST, MusicTrackUtils.getArtists(track));
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);
@@ -1,20 +1,23 @@
package ru.vtosters.lite.music.cache.helpers;
import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId;
import android.net.Uri;
import com.vk.dto.music.Playlist;
import com.vtosters.lite.R;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.util.ArrayList;
import java.util.List;
import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils;
public class PlaylistHelper {
@@ -34,7 +37,7 @@ public class PlaylistHelper {
return createAlbum(
thumb != null ? thumb.h(600) : "",
albumLink.getTitle(),
M3UDownloader.getArtists(track),
MusicTrackUtils.getArtists(track),
track.y1(),
albumLink.getId()
);
@@ -76,7 +79,7 @@ public class PlaylistHelper {
list.add(createAlbum(
thumb != null ? thumb.h(600) : "",
albumLink.getTitle(),
M3UDownloader.getArtists(track),
MusicTrackUtils.getArtists(track),
track.y1(),
albumLink.getId()
));
@@ -1,9 +1,12 @@
package ru.vtosters.lite.music.callback;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.vtosters.lite.R;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.utils.AndroidUtils;
@@ -40,12 +43,12 @@ public class MusicCallbackBuilder {
} catch (UnsatisfiedLinkError e) {
Log.e("AudioDownloader", "native libs");
Log.e("AudioDownloader", e.getMessage());
onFailure();
onFailure(e);
}
}
@Override
public void onFailure() {
public void onFailure(Throwable e) {
notification.setContentText(AndroidUtils.getString(R.string.load_audio_error)).setProgress(0, 0, false);
notificationManager.notify(notificationId, notification.build());
}
@@ -78,7 +81,7 @@ public class MusicCallbackBuilder {
}
@Override
public void onFailure() {
public void onFailure(Throwable e) {
notification.setContentText(AndroidUtils.getString(R.string.load_audio_error)).setProgress(0, 0, false);
notificationManager.notify(notificationId, notification.build());
}
@@ -2,26 +2,30 @@ package ru.vtosters.lite.music.converter.ts;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class MpegDemuxer {
public static void convert(File in, String out, MusicTrack track) throws IOException {
public static void convert(byte[] inBytes, String out) throws IOException {
try (InputStream inStream = new ByteArrayInputStream(inBytes)) {
convert(inStream, out);
}
}
public static void convert(InputStream in, String out) throws IOException {
int packetSize = 188;
int desiredPid = 256;
var buffer = new byte[packetSize];
var packet = ByteBuffer.wrap(buffer).order(ByteOrder.BIG_ENDIAN);
try (var fis = new BufferedInputStream(new FileInputStream(in)); var fos = new BufferedOutputStream(new FileOutputStream(out))) {
try (var fis = new BufferedInputStream(in); var fos = new BufferedOutputStream(new FileOutputStream(out))) {
while (fis.read(buffer) != -1) {
packet.clear();
@@ -1,70 +0,0 @@
package ru.vtosters.lite.music.converter.ts;
import android.os.Build;
import ru.vtosters.lite.utils.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Comparator;
public class TSMerger {
/**
* Merge multiple .ts files into one .ts file
*
* @param in Directory with .ts files
*/
public static void merge(File in, File out) {
File[] tsesList = in.listFiles();
if (tsesList == null) {
return;
}
ByteArrayOutputStream mergedByteArrayStream = new ByteArrayOutputStream();
sortTsesList(tsesList);
for (File file : tsesList) {
if (file.getName().endsWith(".ts")) {
try {
mergedByteArrayStream.write(IOUtils.readFully(file));
} catch (IOException e) {
return;
}
}
}
writeOutputFile(out, mergedByteArrayStream);
}
private static void sortTsesList(File[] tsesList) {
Arrays.sort(tsesList, Comparator.comparingInt(TSMerger::getFileIndex));
}
private static int getFileIndex(File file) {
String fileName = file.getName();
int dotIndex = fileName.indexOf('.', 1);
return Integer.parseInt(fileName.substring(0, dotIndex));
}
private static void writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
byte[] bytes = mergedByteArrayStream.toByteArray();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
Files.write(out.toPath(), bytes);
} catch (IOException ignored) {
// ignored
}
} else {
try (FileOutputStream fos = new FileOutputStream(out)) {
fos.write(bytes);
} catch (IOException ignored) {
// ignored
}
}
}
}
@@ -0,0 +1,55 @@
package ru.vtosters.lite.music.downloader;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
public final class CachedDownloader implements ITrackDownloader {
private final ITrackDownloader origin;
public CachedDownloader(ITrackDownloader origin) {
this.origin = origin;
}
@Override
public void download(MusicTrack track, Callback callback) {
origin.download(track, new Callback() {
@Override
public void onProgress(int progress) {
callback.onProgress(progress);
}
@Override
public void onSuccess() {
new ThumbnailDownloader().download(track, new Callback() {
@Override public void onProgress(int progress) {}
@Override public void onSuccess() {}
@Override
public void onFailure(Throwable e) {
callback.onFailure(e);
}
@Override public void onSizeReceived(long size, long header) {}
});
MusicCacheImpl.addTrack(track);
callback.onSuccess();
}
@Override
public void onFailure(Throwable e) {
callback.onFailure(e);
}
@Override
public void onSizeReceived(long size, long header) {
callback.onSizeReceived(size, header);
}
});
}
}
@@ -20,6 +20,7 @@ public class ID3Tagger {
public static void tag(File file, MusicTrack track) {
try {
// https://github.com/mpatric/mp3agic/issues/69#issuecomment-57897874
// TODO: get rid of file duplication
var oldFile = new File(file.getAbsolutePath().replace(".mp3", "_old.mp3"));
if (!file.renameTo(oldFile)) {
Log.e("ID3Tagger", "Failed to rename file");
@@ -38,7 +39,7 @@ public class ID3Tagger {
id3v2Tag.setAlbum(track.I.getTitle());
}
id3v2Tag.setTitle(M3UDownloader.getTitle(track));
id3v2Tag.setTitle(Mp3Downloader.getTitle(track));
mp3File.setId3v2Tag(id3v2Tag);
mp3File.save(file.getAbsolutePath());
@@ -1,199 +0,0 @@
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.Artist;
import com.vk.dto.music.MusicTrack;
import java8.util.concurrent.CompletableFuture;
import java8.util.concurrent.CompletionException;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.converter.ts.MpegDemuxer;
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;
import java.util.stream.Collectors;
public class M3UDownloader
implements ITrackDownloader {
public static M3UDownloader getInstance() {
return Holder.INSTANCE;
}
static void parse(
File outDir,
Callback callback,
MusicTrack track,
boolean cache)
throws IOException {
var parser = new com.google.android.exoplayer2.source.hls.playlist.h(e.a(track.D));
var baseUri = track.D.substring(0, track.D.lastIndexOf("/") + 1);
var segments = ((f) parser.a(Uri.parse(baseUri), IOUtils.openStream(track.D))).o;
var progress = new AtomicInteger();
callback.onProgress(5);
var resultTs = getResultTsFile(track);
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 {
MpegDemuxer.convert(resultTs, resultMp3.getAbsolutePath(), track);
} catch (Throwable e) {
throw new RuntimeException("MpegDemuxer error", e);
}
})
.thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size())))
.thenRun(() -> {
if (Preferences.getBoolValue("setMetaData", true)) {
ID3Tagger.tag(resultMp3, track);
}
})
.thenRun(() -> {
if (cache) {
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(
String baseUri,
List<a> segments,
AtomicInteger progress,
Callback callback,
MusicTrack track) {
var futures = new CompletableFuture[segments.size()];
for (var i = 0; i < segments.size(); ++i) {
var segment = segments.get(i);
var filename = i + ".ts";
futures[i] = CompletableFuture.runAsync(() -> processSegment(baseUri, segment, filename, progress, segments.size(), callback, track));
}
return futures;
}
private static void processSegment(
String baseUri,
a segment,
String filename,
AtomicInteger progress,
int size,
Callback callback,
MusicTrack track) {
try {
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
if (!TextUtils.isEmpty(segment.g)) {
var cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
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);
}
}
public static String getTitle(MusicTrack track) {
return track.f + (!TextUtils.isEmpty(track.g) ? +'(' + track.g + ')' : "");
}
static File getTsesDir(MusicTrack track) {
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
var tsesDir = new File(filesDir, String.valueOf(track.d));
tsesDir.mkdirs();
return tsesDir;
}
static File getResultTsFile(MusicTrack track) {
return new File(getTsesDir(track), "result.ts");
}
static File getResultMp3File(File outDir, boolean cache, MusicTrack track) {
return new File(outDir, IOUtils.getValidFileName((cache ? "track" : getArtists(track) + " - " + getTitle(track)) + ".mp3"));
}
@Override
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 {
parse(outDir, callback, track, cache);
} catch (IOException e) {
callback.onFailure();
Log.e("M3UDownloader", e + "");
throw new RuntimeException(e);
}
}
// Initialization-on-demand
private static class Holder {
private static final M3UDownloader INSTANCE = new M3UDownloader();
}
public static String getArtists(MusicTrack track) {
return (track.L == null || track.I == null
? normalizeMetadata(track.C)
: normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))));
}
private static String normalizeMetadata(String in) {
return in.replaceAll("[\\\\/:*?\"<>|]", "");
}
}
@@ -1,76 +0,0 @@
//package ru.vtosters.lite.music;
//
//import android.text.TextUtils;
//import android.util.Log;
//import android.widget.Toast;
//
//import com.vk.dto.music.MusicTrack;
//
//import org.json.JSONException;
//import org.json.JSONObject;
//
//import java.io.File;
//import java.io.IOException;
//
//import okhttp3.Call;
//import okhttp3.Callback;
//import okhttp3.OkHttpClient;
//import okhttp3.Request;
//import okhttp3.Response;
//import ru.vtosters.lite.downloaders.AudioDownloader;
//import ru.vtosters.lite.music.interfaces.ITrackDownloader;
//import ru.vtosters.lite.utils.AndroidUtils;
//
//public class MP3Downloader implements ITrackDownloader {
// private static final String URL_FORMAT = "https://api.vk.com/method/audio.getById?v=5.131&access_token=%s&audios=%s&copyright=";
//
// private static final OkHttpClient client = new OkHttpClient();
//
// public static void downloadMP3(MusicTrack track) {
// var token = AndroidUtils.getPreferences().getString("vk_admin_token", "");
// if (token.isEmpty()) {
// Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show();
// return;
// }
//
// var request = new Request.a()
// .b(String.format(URL_FORMAT, token, track.y1()))
// .a();
// client.a(request).a(new Callback() {
// @Override
// public void a(Call call, IOException e) {
// Log.d("MP3Downloader", e + "");
// }
//
// @Override
// public void a(Call call, Response response) throws IOException {
// var payload = response.a().g();
// Log.d("MP3Downloader", payload);
// try {
// JSONObject jsonObj = new JSONObject(payload);
// if (!jsonObj.has("response"))
// return;
// JSONObject responseNode = new JSONObject(payload).getJSONArray("response")
// .getJSONObject(0);
// if (!responseNode.has("url"))
// return;
// var url = responseNode.optString("url");
// Log.d("MP3Downloader", url);
// if (TextUtils.isEmpty(url))
// return;
// if (VKM3UParser.isM3U8(url)) {
// AudioDownloader.downloadM3U8(track);
// return;
// }
// } catch (JSONException e) {
// Log.d("MP3Downloader", e + "");
// }
// }
// });
// }
//
// @Override
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.interfaces.Callback callback) {
//
// }
//}
@@ -0,0 +1,97 @@
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 java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.converter.ts.MpegDemuxer;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.IOUtils;
public final class Mp3Downloader implements ITrackDownloader {
private final File outputFile;
public Mp3Downloader(File outputFile) {
this.outputFile = outputFile;
}
@Override
public void download(MusicTrack track, Callback callback) {
String uri = track.D;
if (Objects.requireNonNull(uri).isEmpty()) {
String msg = "link error: " + track.y1() + ", title: " + Mp3Downloader.getTitle(track);
Log.d("Mp3Downloader", msg);
throw new RuntimeException(msg);
}
var tsParser = new com.google.android.exoplayer2.source.hls.playlist.h(e.a(track.D));
var baseUri = track.D.substring(0, track.D.lastIndexOf("/") + 1);
try {
var segments = ((f) tsParser.a(Uri.parse(baseUri), IOUtils.openStream(track.D))).o;
byte[] buff = getMergedTs(baseUri, segments, callback);
MpegDemuxer.convert(buff, outputFile.getAbsolutePath());
if (Preferences.getBoolValue("setMetaData", false)) {
ID3Tagger.tag(outputFile, track);
}
callback.onSuccess();
} catch (IOException | GeneralSecurityException e) {
callback.onFailure(e);
}
}
private static byte[] getMergedTs(String baseUri, List<a> segments, Callback callback) throws IOException, GeneralSecurityException {
byte[] total = new byte[0];
int size = segments.size();
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
int progress = 0;
for (var segment : segments) {
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
if (!TextUtils.isEmpty(segment.g)) {
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
if (cipherBytes.length != 16) cipherBytes = new byte[16];
var cipherIv = new IvParameterSpec(cipherBytes);
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
buff = cipher.doFinal(buff);
}
// copy on write
final int len = total.length, n = buff.length;
byte[] newBytes = Arrays.copyOf(total, len + n);
System.arraycopy(buff, 0, newBytes, len, n);
total = newBytes;
callback.onProgress(10 + Math.round(80.0F * (++progress) / size));
}
return total;
}
static String getTitle(MusicTrack track) {
return track.f + (!TextUtils.isEmpty(track.g) ? '(' + track.g + ')' : "");
}
}
@@ -1,63 +1,68 @@
package ru.vtosters.lite.music.downloader;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.interfaces.Callback;
import java.io.File;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java8.util.concurrent.CompletableFuture;
import ru.vtosters.lite.music.interfaces.Callback;
public class PlaylistDownloader {
public static void downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
public static CompletableFuture<Void>
downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
var outDir = new File(path);
if (!outDir.exists())
if (outDir.mkdirs()) Log.v("PlaylistDownloader", "Directory created");
else Log.e("PlaylistDownloader", "Directory creation failed");
var downloadedTracks = new AtomicInteger();
for (var musicTrack : playlist)
TrackDownloader.downloadTrack(musicTrack, path, new Callback() {
@Override
public void onProgress(int progress) {
Callback delegate = new ProgressCallback(callback);
return CompletableFuture.allOf(playlist
.stream()
.map(x -> {
Callback.CompletableFutureCallback d = new
Callback.CompletableFutureCallback(delegate);
TrackDownloader.downloadTrack(x, path, d);
return d;
})
.toArray(CompletableFuture[]::new));
}
public static CompletableFuture<Void> cachePlaylist(List<MusicTrack> playlist, Callback callback) {
Callback delegate = new ProgressCallback(callback);
return CompletableFuture.allOf(playlist
.stream()
.map(x -> {
Callback.CompletableFutureCallback d = new
Callback.CompletableFutureCallback(delegate);
TrackDownloader.cacheTrack(x, d);
return d;
})
.toArray(CompletableFuture[]::new));
}
public static final class ProgressCallback implements Callback {
private final AtomicInteger progress = new AtomicInteger();
private final Callback origin;
public ProgressCallback(Callback origin) {
this.origin = origin;
}
@Override public void onProgress(int progress) {}
@Override
public void onSuccess() {
callback.onProgress(downloadedTracks.incrementAndGet());
origin.onProgress(progress.incrementAndGet());
}
@Override
public void onFailure() {
callback.onFailure();
public void onFailure(Throwable e) {
origin.onFailure(e);
}
@Override
public void onSizeReceived(long size, long header) {
}
});
}
public static void cachePlaylist(List<MusicTrack> playlist, Callback callback) {
var downloadedTracks = new AtomicInteger();
for (var musicTrack : playlist)
TrackDownloader.cacheTrack(musicTrack, new Callback() {
@Override
public void onProgress(int progress) {
}
@Override
public void onSuccess() {
callback.onProgress(downloadedTracks.incrementAndGet());
}
@Override
public void onFailure() {
callback.onFailure();
}
@Override
public void onSizeReceived(long size, long header) {
}
});
@Override public void onSizeReceived(long size, long header) {}
}
}
@@ -1,18 +1,23 @@
package ru.vtosters.lite.music.downloader;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack;
import org.json.JSONArray;
import org.json.JSONObject;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.IOException;
import java.net.URL;
public class ThumbnailDownloader {
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
public static void downloadThumbnails(MusicTrack track) throws IOException {
public final class ThumbnailDownloader implements ITrackDownloader {
@Override
public void download(MusicTrack track, Callback callback) {
JSONObject json = track.J();
JSONObject album = json.optJSONObject("album");
if (album == null) return;
@@ -20,6 +25,7 @@ public class ThumbnailDownloader {
if (thumb == null) return;
JSONArray sizes = thumb.optJSONArray("sizes");
String trackId = LibVKXClient.asId(track);
try {
if (sizes == null) {
JSONArray names = thumb.names();
if (names == null) return;
@@ -35,9 +41,14 @@ public class ThumbnailDownloader {
downloadThumbnail(src, width, trackId);
}
}
callback.onSuccess();
} catch (IOException e) {
callback.onFailure(e);
throw new RuntimeException(e);
}
}
private static void downloadThumbnail(String url, int res, String trackId) throws IOException {
IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId, res), IOUtils.readFully(new URL(url).openStream()));
}
}
@@ -1,51 +1,45 @@
package ru.vtosters.lite.music.downloader;
import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File;
public class TrackDownloader {
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import ru.vtosters.lite.utils.music.MusicTrackUtils;
public final class TrackDownloader {
private TrackDownloader() { }
public static void downloadTrack(MusicTrack track, String path, Callback callback) {
if (track.D.isEmpty()) {
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
callback.onFailure();
return;
}
download(track, path, callback, M3UDownloader.getInstance(), false);
}
public static void cacheTrack(MusicTrack track, Callback callback) {
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) {
callback.onSuccess();
return;
} else if (track.D.isEmpty()) {
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
callback.onFailure();
return;
}
var path = MusicCacheStorageUtils.getTrackDirById(LibVKXClient.asId(track)).getAbsolutePath();
download(track, path, callback, M3UDownloader.getInstance(), true);
}
private static void download(MusicTrack track, String path, Callback callback, ITrackDownloader downloader, boolean cache) {
File outDir = new File(path);
if (!outDir.exists())
if (!outDir.exists()) {
if (outDir.mkdir())
Log.v("TrackDownloader", "Directory created");
else
Log.e("TrackDownloader", "Directory creation failed");
}
var outputFile = new File(outDir, IOUtils.getValidFileName(
MusicTrackUtils.getArtists(track) + " - " + Mp3Downloader.getTitle(track)) + ".mp3");
VTExecutors.getMusicDownloadExecutor().execute(
() -> downloader.downloadTrack(track, outDir, callback, cache)
);
VTExecutors.getMusicDownloadExecutor().submit(() -> new Mp3Downloader(outputFile).download(track, callback));
}
public static void cacheTrack(MusicTrack track, Callback callback) {
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track)))
return;
var outputFile = MusicCacheStorageUtils
.getTrackFile(LibVKXClient.asId(track));
VTExecutors.getMusicDownloadExecutor().submit(() -> new CachedDownloader(
new Mp3Downloader(outputFile)
).download(track, callback));
}
}
@@ -1,11 +1,46 @@
package ru.vtosters.lite.music.interfaces;
import java8.util.concurrent.CompletableFuture;
public interface Callback {
void onProgress(int progress);
void onSuccess();
void onFailure();
void onFailure(Throwable e);
void onSizeReceived(long size, long header);
class CompletableFutureCallback
extends CompletableFuture<Void>
implements Callback {
private final Callback origin;
public CompletableFutureCallback(Callback origin) {
this.origin = origin;
}
@Override
public void onProgress(int progress) {
origin.onProgress(progress);
}
@Override
public void onSuccess() {
complete(null);
origin.onSuccess();
}
@Override
public void onFailure(Throwable e) {
completeExceptionally(e);
origin.onFailure(e);
}
@Override
public void onSizeReceived(long size, long header) {
origin.onSizeReceived(size, header);
}
}
}
@@ -2,8 +2,6 @@ package ru.vtosters.lite.music.interfaces;
import com.vk.dto.music.MusicTrack;
import java.io.File;
/**
* Interface for track downloaders
*/
@@ -12,8 +10,7 @@ public interface ITrackDownloader {
* Download track
*
* @param track Music track
* @param outDir Directory where to download the track
* @param callback Callback to call when download status changes
*/
void downloadTrack(MusicTrack track, File outDir, Callback callback, boolean cache);
void download(MusicTrack track, Callback callback);
}
@@ -0,0 +1,18 @@
package ru.vtosters.lite.utils.music;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Artist;
import java.util.stream.Collectors;
public class MusicTrackUtils {
public static String getArtists(MusicTrack musicTrack) {
return (musicTrack.L == null || musicTrack.I == null
? normalizeMetadata(musicTrack.C)
: normalizeMetadata(musicTrack.L.stream().map(Artist::w1).collect(Collectors.joining(", "))));
}
private static String normalizeMetadata(String in) {
return in.replaceAll("[\\\\/:*?\"<>|]", "");
}
}