diff --git a/app/build.gradle b/app/build.gradle index 741f1a3b0..ceee3b902 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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']) diff --git a/app/libs/ffmpeg-kit.aar b/app/libs/ffmpeg-kit.aar deleted file mode 100644 index 178c2affc..000000000 Binary files a/app/libs/ffmpeg-kit.aar and /dev/null differ diff --git a/app/src/main/java/ru/vtosters/hooks/music/injectors/PlaylistInjector.java b/app/src/main/java/ru/vtosters/hooks/music/injectors/PlaylistInjector.java index 4d2996969..d01149529 100644 --- a/app/src/main/java/ru/vtosters/hooks/music/injectors/PlaylistInjector.java +++ b/app/src/main/java/ru/vtosters/hooks/music/injectors/PlaylistInjector.java @@ -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() { diff --git a/app/src/main/java/ru/vtosters/lite/concurrent/VTExecutors.java b/app/src/main/java/ru/vtosters/lite/concurrent/VTExecutors.java index 29bb60e06..c8789e22d 100644 --- a/app/src/main/java/ru/vtosters/lite/concurrent/VTExecutors.java +++ b/app/src/main/java/ru/vtosters/lite/concurrent/VTExecutors.java @@ -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)); - } - } - } } diff --git a/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java b/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java index 48470db61..668db7be0 100644 --- a/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java @@ -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 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 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); } } diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheDbDelegate.java b/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheDbDelegate.java index 3dba6e124..1da2bc462 100644 --- a/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheDbDelegate.java +++ b/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheDbDelegate.java @@ -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, diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java deleted file mode 100644 index dff3229e6..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java +++ /dev/null @@ -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("[\\\\/:*?\"<>|]", ""); - } -} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/CachedDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/CachedDownloader.java new file mode 100644 index 000000000..3d5472066 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/CachedDownloader.java @@ -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); + } + }); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/M3UDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/M3UDownloader.java deleted file mode 100644 index e5fee55f3..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/M3UDownloader.java +++ /dev/null @@ -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 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(); - } -} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/MP3Downloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/MP3Downloader.java deleted file mode 100644 index 72e6c7157..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/MP3Downloader.java +++ /dev/null @@ -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©right="; -// -// 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) { -// -// } -//} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/Mp3Downloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/Mp3Downloader.java new file mode 100644 index 000000000..62a1ef444 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/Mp3Downloader.java @@ -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 + ')' : ""); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java index dbb1d9a35..a9de892a8 100644 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java @@ -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 playlist, String playlistName, String path, Callback callback) { + public static CompletableFuture + downloadPlaylist(List 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 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 cachePlaylist(List 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) {} } } \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java index 061966471..f92e3f9a4 100644 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java @@ -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); } } diff --git a/app/src/main/java/ru/vtosters/lite/music/interfaces/Callback.java b/app/src/main/java/ru/vtosters/lite/music/interfaces/Callback.java index a0f9af70d..c4ad96d53 100644 --- a/app/src/main/java/ru/vtosters/lite/music/interfaces/Callback.java +++ b/app/src/main/java/ru/vtosters/lite/music/interfaces/Callback.java @@ -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 + 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); + } + } } diff --git a/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java b/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java index 92aa271c5..df670d46f 100644 --- a/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java @@ -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); } \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java index f3e554cbb..57c151167 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java @@ -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(); }