transfer audio download to ffmpeg

This commit is contained in:
sunmisc
2024-02-15 16:36:29 +03:00
parent 404594df87
commit 287af14589
16 changed files with 259 additions and 428 deletions
+1
View File
@@ -96,6 +96,7 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation 'net.sourceforge.streamsupport:streamsupport-minifuture:1.7.4'
implementation 'com.google.guava:guava:23.3-android'
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2'
implementation fileTree(dir: 'libs', include: ['*.aar'])
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
Binary file not shown.
@@ -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() {
@@ -4,12 +4,8 @@ import com.vk.core.concurrent.VkExecutors;
import io.reactivex.Scheduler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
public class VTExecutors {
public static ExecutorService getMusicDownloadExecutor() {
return VTMusicDownloadExecutor.getInstance();
}
public static ExecutorService getIoExecutor() {
return VkExecutors.x.e();
@@ -22,30 +18,4 @@ public class VTExecutors {
public static Scheduler getSlowTasksScheduler() {
return VkExecutors.x.q();
}
static class VTMusicDownloadExecutor {
private VTMusicDownloadExecutor() {
}
public static ExecutorService getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final int PARALLELISM = clamp(
(Runtime.getRuntime().availableProcessors() >> 1) - 1,
1, 7);
private static final ExecutorService INSTANCE =
new ForkJoinPool(PARALLELISM,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
(t, e) -> {},
/* FIFO */ true);
private static int clamp(int value, int min, int max) {
return Math.min(max, Math.max(value, min));
}
}
}
}
@@ -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;
@@ -34,7 +35,7 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
public class AudioDownloader {
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);
@@ -45,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,
@@ -64,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();
downloadM3U8(track, true);
}
var trackFile = MusicCacheFilesHook.getTrackFile(trackId);
if (!trackFile.exists())
trackFile.getParentFile().mkdirs();
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)
);
@@ -116,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);
}
}
@@ -1,16 +1,16 @@
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) {
return new MusicCacheDb(context);
}
private static MusicCacheDb connectToDb(Context context) {
return new MusicCacheDb(context);
}
public static void addTrack(Context context,
@@ -1,48 +0,0 @@
package ru.vtosters.lite.music.converter.ts;
import com.arthenica.ffmpegkit.FFmpegKit;
import com.vk.dto.music.Artist;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.downloader.M3UDownloader;
import ru.vtosters.lite.utils.IOUtils;
import java.io.File;
import java.util.stream.Collectors;
public class FFMpeg {
/**
* @param in Merged .ts file via {@link ru.vtosters.lite.music.converter.ts.TSMerger}
* @param out Output .mp3 file
* @param track Track to be converted
* @return True if success, else false
*/
public static void convert(File in, String out, MusicTrack track) {
// convert .ts file to .mp3 file via ffmpeg
var cmd = " -y -i \"" + in.getAbsolutePath() + '"'
+ " -map 0 -dn"
+ " -loglevel error"
+ " -hide_banner"
+ " -write_id3v2 1"
+ setMetadata(track)
+ " -c copy \"" + out + '"';
var ses = FFmpegKit.execute(cmd);
if (ses.getReturnCode().isValueCancel()) throw new RuntimeException(ses.getLogsAsString());
}
private static String setMetadata(MusicTrack track) {
if (!Preferences.getBoolValue("setMetaData", false)) {
return "";
}
return " -metadata title=\"" + IOUtils.getValidFileName(M3UDownloader.getTitle(track)) + '"'
+ (track.L == null || track.I == null
? " -metadata artist=\"" + normalizeMetadata(track.C) + '"'
: " -metadata artist=\"" + normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))) + '"'
+ " -metadata album=\"" + normalizeMetadata(track.I.getTitle()) + '"');
}
private static String normalizeMetadata(String in) {
return in.replaceAll("[\\\\/:*?\"<>|]", "");
}
}
@@ -0,0 +1,34 @@
package ru.vtosters.lite.music.downloader;
import com.vk.dto.music.MusicTrack;
import java.io.IOException;
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 downloadTrack(MusicTrack track, Callback callback) {
var d = new Callback.CompletableFutureCallback(callback);
origin.downloadTrack(track, d);
d.thenAccept(result -> {
try {
ThumbnailDownloader.downloadThumbnails(track);
MusicCacheImpl.addTrack(track);
} catch (IOException e) {
callback.onFailure();
throw new RuntimeException(e);
}
});
}
}
@@ -1,153 +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.MusicTrack;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.converter.ts.FFMpeg;
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 javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
public class M3UDownloader implements ITrackDownloader {
public static M3UDownloader getInstance() {
return Holder.INSTANCE;
}
private 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;
callback.onProgress(5);
var resultTs = getResultTsFile(track);
var resultMp3 = getResultMp3File(outDir, cache, track);
try {
byte[] buff = downloadTs(baseUri, segments, callback);
IOUtils.writeToFile(resultTs, buff);
if (cache) {
try {
ThumbnailDownloader.downloadThumbnails(track);
} catch (IOException e) {
throw new RuntimeException("Failed to download thumbs", e);
}
}
try {
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
} catch (Throwable e) {
throw new RuntimeException("FFmpeg error", e);
}
int sz = segments.size();
callback.onProgress(10 + Math.round(80.0F * (sz + 1F) / sz));
if (cache) {
try {
MusicCacheImpl.addTrack(track);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} catch (GeneralSecurityException e) {
Log.e("M3UDownloader", "Failed to download track", e.getCause());
} finally {
resultTs.delete();
}
}
private static byte[] downloadTs(String baseUri, List<a> segments, Callback callback) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException {
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;
}
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" : track.C + " - " + 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);
callback.onSuccess();
} catch (IOException e) {
callback.onFailure();
Log.e("M3UDownloader", String.valueOf(e));
throw new RuntimeException(e);
}
}
// Initialization-on-demand
private static class Holder {
private static final M3UDownloader INSTANCE = new M3UDownloader();
}
}
@@ -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,75 @@
package ru.vtosters.lite.music.downloader;
import android.text.TextUtils;
import android.util.Log;
import com.arthenica.ffmpegkit.FFmpegKit;
import com.vk.dto.music.Artist;
import com.vk.dto.music.MusicTrack;
import java.io.File;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import ru.vtosters.hooks.other.Preferences;
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 outDir;
public Mp3Downloader(File outDir) {
this.outDir = outDir;
}
@Override
public void downloadTrack(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("TrackDownloader", msg);
throw new RuntimeException(msg);
}
var cmd = " -y -http_persistent false -i \"" + uri + '"'
+ " -map 0 -dn"
+ " -loglevel error"
+ " -hide_banner"
+ " -write_id3v2 1"
+ setMetadata(track)
+ " -c copy \"" + outDir.delete() + '"';
// todo: make a normal progression system
final AtomicInteger progress = new AtomicInteger();
FFmpegKit.executeAsync(cmd, session -> {
if (session.getReturnCode().isValueSuccess()) {
callback.onProgress(100);
callback.onSuccess();
} else
callback.onFailure();
}, log -> callback.onProgress(Math.min(100, progress.incrementAndGet())),
statistics -> {}
);
}
private static String setMetadata(MusicTrack track) {
if (!Preferences.getBoolValue("setMetaData", false)) {
return "";
}
return " -metadata title=\"" + IOUtils.getValidFileName(Mp3Downloader.getTitle(track)) + '"'
+ (track.L == null || track.I == null
? " -metadata artist=\"" + normalizeMetadata(track.C) + '"'
: " -metadata artist=\"" + normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))) + '"'
+ " -metadata album=\"" + normalizeMetadata(track.I.getTitle()) + '"');
}
private static String normalizeMetadata(String in) {
return in.replaceAll("[\\\\/:*?\"<>|]", "");
}
private 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) {
}
@Override
public void onSuccess() {
callback.onProgress(downloadedTracks.incrementAndGet());
}
@Override
public void onFailure() {
callback.onFailure();
}
@Override
public void onSizeReceived(long size, long header) {
}
});
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 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) {
}
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));
}
@Override
public void onSuccess() {
callback.onProgress(downloadedTracks.incrementAndGet());
}
public static final class ProgressCallback implements Callback {
private final AtomicInteger progress = new AtomicInteger();
private final Callback origin;
@Override
public void onFailure() {
callback.onFailure();
}
public ProgressCallback(Callback origin) {
this.origin = origin;
}
@Override
public void onSizeReceived(long size, long header) {
}
});
@Override public void onProgress(int progress) {}
@Override
public void onSuccess() {
origin.onProgress(progress.incrementAndGet());
}
@Override
public void onFailure() {
origin.onFailure();
}
@Override public void onSizeReceived(long size, long header) {}
}
}
@@ -1,51 +1,41 @@
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 {
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;
}
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
download(track, path, callback, M3UDownloader.getInstance(), false);
}
public final class TrackDownloader {
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;
}
private TrackDownloader() { }
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) {
public static void
downloadTrack(MusicTrack track, String path, Callback callback) {
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");
}
new Mp3Downloader(outDir).downloadTrack(track, callback);
}
VTExecutors.getMusicDownloadExecutor().execute(
() -> downloader.downloadTrack(track, outDir, callback, cache)
);
public static void
cacheTrack(MusicTrack track, Callback callback) {
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track)))
return;
var path = MusicCacheStorageUtils
.getTrackDirById(LibVKXClient.asId(track));
new CachedDownloader(
new Mp3Downloader(path)
).downloadTrack(track, callback);
}
}
@@ -1,5 +1,7 @@
package ru.vtosters.lite.music.interfaces;
import java8.util.concurrent.CompletableFuture;
public interface Callback {
void onProgress(int progress);
@@ -8,4 +10,37 @@ public interface Callback {
void onFailure();
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() {
completeExceptionally(new RuntimeException("callback failed"));
origin.onFailure();
}
@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 downloadTrack(MusicTrack track, Callback callback);
}
@@ -9,15 +9,21 @@ import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.preference.Preference;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vk.core.network.Network;
import com.vtosters.lite.R;
import org.json.JSONObject;
import java.util.Arrays;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.hooks.other.ThemesUtils;
import ru.vtosters.lite.concurrent.VTExecutors;
@@ -32,13 +38,7 @@ import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.LifecycleUtils;
import ru.vtosters.lite.utils.SearchEngine;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
private static final ExecutorService executor = Executors.newCachedThreadPool();
public static void download(Context ctx) {
final EditText input = new EditText(ctx);
input.setTextColor(ThemesUtils.getTextAttr());
@@ -221,7 +221,9 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
.setTitle(R.string.warning)
.setMessage(R.string.cached_tracks_remove_confirm)
.setPositiveButton(R.string.yes,
(dialog, which) -> executor.submit(MusicCacheImpl::clear))
(dialog, which) -> VTExecutors
.getIoExecutor()
.execute(MusicCacheImpl::clear))
.setNeutralButton(R.string.no,
(dialog, which) -> dialog.cancel())
.show();
@@ -237,10 +239,12 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
.setTitle(R.string.download_method)
.setMessage(R.string.download_method_desc)
.setPositiveButton(R.string.download_method_cache, (dialog, which) -> {
executor.submit(AudioDownloader::cacheAllAudios);
VTExecutors
.getIoExecutor()
.execute(AudioDownloader::cacheAllAudios);
})
.setNegativeButton(R.string.download_method_mp3, (dialog, which) -> {
executor.submit(AudioDownloader::downloadAllAudios);
AudioDownloader.downloadAllAudios();
})
.show();
}