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 537166b6a..dd9d1984a 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 @@ -24,9 +24,10 @@ import java.util.concurrent.Executors; public class PlaylistInjector { private static final ExecutorService executor = Executors.newCachedThreadPool(); + public final static String CHANNEL_NAME = "VTCH"; 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 2c34f81e3..bde4383b1 100644 --- a/app/src/main/java/ru/vtosters/lite/concurrent/VTExecutors.java +++ b/app/src/main/java/ru/vtosters/lite/concurrent/VTExecutors.java @@ -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)); + } } } } 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 2f2bd8e46..ef28243b7 100644 --- a/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java @@ -7,6 +7,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; @@ -22,8 +24,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,10 +35,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 downloadPlaylist(Playlist playlist) { var tracks = PlaylistConverter.getPlaylist(playlist); var playlistName = IOUtils.getValidFileName(playlist.g); @@ -48,7 +48,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, @@ -67,16 +67,15 @@ 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(); - executor.submit(() -> 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(); @@ -86,7 +85,7 @@ public class AudioDownloader { Log.d("Playlist", "adding to cache " + playlist.a); - PlaylistDownloader.cachePlaylist( + return PlaylistDownloader.cachePlaylist( tracks, MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId), playlist.v1() @@ -125,22 +124,23 @@ 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); + var cb = MusicCallbackBuilder.buildOneTrackCallback(tempId, notification); if (cache) { - TrackDownloader.cacheTrack(track, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification)); + TrackDownloader.cacheTrack(track, cb); PlaylistCacheDbDelegate.addTrackToPlaylist(AndroidUtils.getGlobalContext(), AccountManagerUtils.getUserId() + "_-1", track.y1()); // TODO Move to download success } else { - TrackDownloader.downloadTrack(track, downloadPath, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification)); + TrackDownloader.downloadTrack(track, downloadPath, cb); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/music/Genius.java b/app/src/main/java/ru/vtosters/lite/music/Genius.java index 53cbdec95..d88158c32 100644 --- a/app/src/main/java/ru/vtosters/lite/music/Genius.java +++ b/app/src/main/java/ru/vtosters/lite/music/Genius.java @@ -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; diff --git a/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java b/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java index 5a916c4c2..27738ca29 100644 --- a/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java +++ b/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java @@ -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; diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheImpl.java b/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheImpl.java index b9f5b8de9..ced47736f 100644 --- a/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheImpl.java +++ b/app/src/main/java/ru/vtosters/lite/music/cache/MusicCacheImpl.java @@ -2,6 +2,11 @@ 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; @@ -13,8 +18,7 @@ import ru.vtosters.lite.music.downloader.M3UDownloader; import ru.vtosters.lite.utils.AccountManagerUtils; 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) { @@ -24,7 +28,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, diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/db/MusicCacheDb.java b/app/src/main/java/ru/vtosters/lite/music/cache/db/MusicCacheDb.java index bbdaf4fe3..d89c87275 100644 --- a/app/src/main/java/ru/vtosters/lite/music/cache/db/MusicCacheDb.java +++ b/app/src/main/java/ru/vtosters/lite/music/cache/db/MusicCacheDb.java @@ -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; + /** *

Note: the {@link AutoCloseable} interface was * first added in the {@link android.os.Build.VERSION_CODES#Q} release for the {@link SQLiteOpenHelper}

@@ -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); diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/delegate/MusicCacheDbDelegate.java b/app/src/main/java/ru/vtosters/lite/music/cache/delegate/MusicCacheDbDelegate.java index 8d65bb312..74d6ac22c 100644 --- a/app/src/main/java/ru/vtosters/lite/music/cache/delegate/MusicCacheDbDelegate.java +++ b/app/src/main/java/ru/vtosters/lite/music/cache/delegate/MusicCacheDbDelegate.java @@ -1,16 +1,16 @@ package ru.vtosters.lite.music.cache.delegate; 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/cache/helpers/PlaylistHelper.java b/app/src/main/java/ru/vtosters/lite/music/cache/helpers/PlaylistHelper.java index 148aa7167..f53f45938 100644 --- a/app/src/main/java/ru/vtosters/lite/music/cache/helpers/PlaylistHelper.java +++ b/app/src/main/java/ru/vtosters/lite/music/cache/helpers/PlaylistHelper.java @@ -2,8 +2,12 @@ package ru.vtosters.lite.music.cache.helpers; import android.util.Log; import com.vk.dto.music.MusicTrack; +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; @@ -16,7 +20,10 @@ 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 { @@ -28,7 +35,20 @@ public class PlaylistHelper { } } - public static JSONArray addCachedPlaylists(JSONArray jsonArray, Boolean noPlaylists) { + public static Playlist createCachedPlaylistMetadata(String id) { + var track = TracklistHelper.getTracksOne(id).get(0); + var albumLink = track.I; + var thumb = albumLink.u1(); + return createAlbum( + thumb != null ? thumb.h(600) : "", + albumLink.getTitle(), + MusicTrackUtils.getArtists(track), + track.y1(), + albumLink.getId() + ); + } + + public static Playlist createAlbum(String photo, String title, String artist, String trackId, int albumId) { try { if (noPlaylists) { jsonArray.put(new JSONArray().put(getCachedSongsPlaylist())); @@ -74,6 +94,22 @@ public class PlaylistHelper { .put("count", 0); } + public static List getAlbumPlaylists() { + List list = new ArrayList<>(); + for (var track : MusicCacheImpl.getPlaylist()) { + var albumLink = track.I; + var thumb = albumLink.u1(); + list.add(createAlbum( + thumb != null ? thumb.h(600) : "", + albumLink.getTitle(), + MusicTrackUtils.getArtists(track), + track.y1(), + albumLink.getId() + )); + } + return list; + } + public static JSONObject getCachedSongsPlaylist() throws JSONException { return new JSONObject() .put("id", -1) diff --git a/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java b/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java index ca67484d9..7e4dd188f 100644 --- a/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java +++ b/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java @@ -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()); } diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/MpegDemuxer.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/MpegDemuxer.java index f7302df06..26ad7e330 100644 --- a/app/src/main/java/ru/vtosters/lite/music/converter/ts/MpegDemuxer.java +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/MpegDemuxer.java @@ -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(); diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java deleted file mode 100644 index bc196d5b3..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java +++ /dev/null @@ -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 - } - } - } -} 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..d3549ea2b --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/CachedDownloader.java @@ -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); + } + }); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/ID3Tagger.java b/app/src/main/java/ru/vtosters/lite/music/downloader/ID3Tagger.java index f7c47e58b..1ff91c521 100644 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/ID3Tagger.java +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/ID3Tagger.java @@ -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()); 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 4a6b0ed58..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/M3UDownloader.java +++ /dev/null @@ -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 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("[\\\\/:*?\"<>|]", ""); - } -} \ No newline at end of file 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..5bc542691 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/Mp3Downloader.java @@ -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 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 + ')' : ""); + } +} 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 234157750..30d73fb00 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,6 +1,7 @@ package ru.vtosters.lite.music.downloader; import android.util.Log; + import com.vk.dto.music.MusicTrack; import ru.vtosters.lite.music.cache.delegate.PlaylistCacheDbDelegate; import ru.vtosters.lite.music.interfaces.Callback; @@ -10,57 +11,62 @@ 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, String playlistId) { - 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()); + 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); PlaylistCacheDbDelegate.addTrackToPlaylist(AndroidUtils.getGlobalContext(), playlistId, musicTrack.y1()); - } + return d; + }) + .toArray(CompletableFuture[]::new)); + } - @Override - public void onFailure() { - callback.onFailure(); - } + public static final class ProgressCallback implements Callback { + private final AtomicInteger progress = new AtomicInteger(); + private final Callback origin; - @Override - public void onSizeReceived(long size, long header) { - } - }); + public ProgressCallback(Callback origin) { + this.origin = origin; + } + + @Override public void onProgress(int progress) {} + + @Override + public void onSuccess() { + origin.onProgress(progress.incrementAndGet()); + } + + @Override + public void onFailure(Throwable e) { + origin.onFailure(e); + } + + @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/ThumbnailDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java index a08b94e4b..99f9141a2 100644 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java @@ -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,24 +25,30 @@ public class ThumbnailDownloader { if (thumb == null) return; JSONArray sizes = thumb.optJSONArray("sizes"); String trackId = LibVKXClient.asId(track); - if (sizes == null) { - JSONArray names = thumb.names(); - if (names == null) return; - for (int i = 0; i < names.length(); ++i) { - String name = names.optString(i); - downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId); - } - } else { - for (int i = 0; i < sizes.length(); ++i) { - JSONObject size = sizes.optJSONObject(i); - int width = size.optInt("width"); - String src = size.optString("src"); - downloadThumbnail(src, width, trackId); + try { + if (sizes == null) { + JSONArray names = thumb.names(); + if (names == null) return; + for (int i = 0; i < names.length(); ++i) { + String name = names.optString(i); + downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId); + } + } else { + for (int i = 0; i < sizes.length(); ++i) { + JSONObject size = sizes.optJSONObject(i); + int width = size.optInt("width"); + String src = size.optString("src"); + 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())); } + } 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..23ed0b130 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,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)); } } 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..64e34693b 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,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 + 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); + } + } } 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..a1d33533e 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 download(MusicTrack track, Callback callback); } \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/utils/music/MusicTrackUtils.java b/app/src/main/java/ru/vtosters/lite/utils/music/MusicTrackUtils.java new file mode 100644 index 000000000..31b120557 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/utils/music/MusicTrackUtils.java @@ -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("[\\\\/:*?\"<>|]", ""); + } +}