From 78d30f9546a1b00b1055e06bc46f64146a6f7d4e Mon Sep 17 00:00:00 2001 From: aliveoutside <53598473+aliveoutside@users.noreply.github.com> Date: Sat, 17 Sep 2022 18:43:29 +0300 Subject: [PATCH] [music] The refactoring one class should only do a one thing and do it great --- .../lite/downloaders/AudioDownloader.java | 112 +++++--- .../notifications/NotificationChannels.java | 18 +- .../lite/hooks/ui/MusicBottomSheetHook.java | 7 +- .../ru/vtosters/lite/music/M3UDownloader.java | 50 +++- .../ru/vtosters/lite/music/MP3Downloader.java | 270 +++++------------- .../cache/injectors/PlaylistInjector.java | 4 +- .../converter/playlist/PlaylistConverter.java | 76 +++++ .../lite/music/converter/{ => ts}/FFMpeg.java | 102 +++---- .../music/converter/{ => ts}/TSMerger.java | 2 +- .../music/downloader/PlaylistDownloader.java | 80 ++++++ .../music/downloader/TrackDownloader.java | 27 ++ .../music/interfaces/ITrackDownloader.java | 20 ++ .../java/ru/vtosters/lite/utils/IOUtils.java | 4 + 13 files changed, 457 insertions(+), 315 deletions(-) create mode 100644 app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java rename app/src/main/java/ru/vtosters/lite/music/converter/{ => ts}/FFMpeg.java (77%) rename app/src/main/java/ru/vtosters/lite/music/converter/{ => ts}/TSMerger.java (97%) create mode 100644 app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java create mode 100644 app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java create mode 100644 app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java 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 ca8517661..b2466f329 100644 --- a/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java @@ -1,5 +1,7 @@ package ru.vtosters.lite.downloaders; +import android.app.Notification; +import android.os.Build; import android.os.Environment; import android.util.Log; @@ -8,51 +10,61 @@ import androidx.core.app.NotificationManagerCompat; import com.vk.core.util.ToastUtils; import com.vk.dto.music.MusicTrack; +import com.vk.dto.music.Playlist; import java.io.File; +import java.util.ArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java8.util.concurrent.CompletableFuture; import ru.vtosters.lite.downloaders.notifications.NotificationChannels; import ru.vtosters.lite.music.Callback; -import ru.vtosters.lite.music.converter.FFMpeg; -import ru.vtosters.lite.music.M3UDownloader; -import ru.vtosters.lite.music.MP3Downloader; +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; +import ru.vtosters.lite.music.cache.FileCacheImplementation; +import ru.vtosters.lite.music.converter.playlist.PlaylistConverter; +import ru.vtosters.lite.music.downloader.PlaylistDownloader; +import ru.vtosters.lite.music.downloader.TrackDownloader; import ru.vtosters.lite.utils.AndroidUtils; +import ru.vtosters.lite.utils.IOUtils; + +/** + * Entrypoint for downloading audio + *

+ * The actual code is in {@link ru.vtosters.lite.music} + */ public class AudioDownloader { + private static final Executor executor = Executors.newFixedThreadPool(4); private static NotificationManagerCompat notificationManager = NotificationChannels.getNotificationManager(); - public static void downloadAudio(MusicTrack track) { - if (AndroidUtils.getDefaultPrefs().getBoolean("new_music_downloading_way", false)) { - downloadMP3(track); - } else { - downloadM3U8(track); + public static void downloadPlaylist(Playlist playlist) { + var tracks = PlaylistConverter.getPlaylist(playlist); + PlaylistDownloader.downloadPlaylist(tracks, IOUtils.getValidFileName(playlist.g)); + } + + private static void cachePlaylist(ArrayList playlist) { + for (MusicTrack musicTrack : playlist) { + CompletableFuture.runAsync(() -> cacheTrack(musicTrack), executor); } } - public static void downloadM3U8(MusicTrack track) { + public static void downloadAudio(MusicTrack track) { + downloadM3U8(track); + } + + private static void downloadM3U8(MusicTrack track) { if (track.D == null) { ToastUtils.a(AndroidUtils.getString("link_audio_error")); return; } var musicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath(); - var musicDir = new File(musicPath); - if (!musicDir.exists()) { - if (musicDir.mkdirs()) { - Log.d("AudioDownloader", "Created music dir"); - } else { - Log.e("AudioDownloader", "Failed to create music dir"); - return; - } - } - var tempId = track.d; - var downloadPath = musicPath + File.separator + tempId; - + var downloadPath = musicPath + File.separator; var notification = buildDownloadNotification(track, tempId); - - downloadM3U8(track, downloadPath, new Callback() { + TrackDownloader.downloadTrack(track, downloadPath, new Callback() { long startTime = 0; long elapsedTime = 0; @@ -72,17 +84,11 @@ public class AudioDownloader { @Override public void onSuccess() { try { - var fileName = track.toString(); - var success = FFMpeg.convert(downloadPath, musicPath + File.separator + fileName + ".mp3", track); - if (success) { - notification - .setContentText(AndroidUtils.getString("player_download_finished")) - .setProgress(0, 0, false) - .setSmallIcon(android.R.drawable.stat_sys_download_done); - notificationManager.notify(tempId, notification.build()); - } else { - onFailure(); - } + notification + .setContentText(AndroidUtils.getString("player_download_finished")) + .setProgress(0, 0, false) + .setSmallIcon(android.R.drawable.stat_sys_download_done); + notificationManager.notify(tempId, notification.build()); } catch (UnsatisfiedLinkError e) { Log.e("AudioDownloader", "native libs"); Log.e("AudioDownloader", e.getMessage()); @@ -103,19 +109,33 @@ public class AudioDownloader { }); } - private static void downloadM3U8(MusicTrack track, String path, Callback callback) { - File outDir = new File(path); - if (!outDir.exists()) - if (outDir.mkdir()) - Log.v("AudioDownloader", "Directory created"); - else - Log.e("AudioDownloader", "Directory creation failed"); - Log.i("AudioDownloader", "Downloading audio to " + path); - M3UDownloader.execute(track.D, outDir, callback); + private static void cacheTrack(MusicTrack track) { + var trackId = track.y1(); + if (CacheDatabaseDelegate.isCached(trackId)) + return; + + var trackFile = FileCacheImplementation.getTrackFile(trackId); + if (!trackFile.exists()) + trackFile.getParentFile().mkdirs(); + downloadM3U8(track); + notifySavingToCache(track); + CacheDatabaseDelegate.insertTrack(track); + NotificationChannels.getNotificationManager().cancel(trackId.hashCode()); } - private static void downloadMP3(MusicTrack track) { - MP3Downloader.downloadMP3(track); + private static void notifySavingToCache(MusicTrack track) { + Notification.Builder builder; + + if (Build.VERSION.SDK_INT >= 26) { + builder = new Notification.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_DOWNLOAD_CHANNEL_ID); + builder.setVisibility(Notification.VISIBILITY_PUBLIC); + } else builder = new Notification.Builder(AndroidUtils.getGlobalContext()); + + builder.setSmallIcon(com.vtosters.lite.R.drawable.ic_download_outline_28) + .setShowWhen(false) + .setContentTitle("Закешированный трек: " + track.toString()); + + NotificationChannels.getNotificationManager().notify(track.y1().hashCode(), builder.build()); } private static NotificationCompat.Builder buildDownloadNotification(MusicTrack track, int id) { @@ -130,4 +150,6 @@ public class AudioDownloader { notificationManager.notify(id, notificationBuilder.build()); return notificationBuilder; } + + } diff --git a/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java b/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java index 146379963..0718d198b 100644 --- a/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java +++ b/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java @@ -13,14 +13,20 @@ import ru.vtosters.lite.utils.AndroidUtils; public class NotificationChannels { public static final String MUSIC_DOWNLOAD_CHANNEL_ID = "music_download_channel"; public static final String MUSIC_DOWNLOAD_CHANNEL_NAME = "Загрузка музыки"; - public static final String MUSIC_CACHING_NAME = "Кеширование трека / Обложки"; public static final String MUSIC_DOWNLOAD_CHANNEL_DESCRIPTION = "Прогресс загрузки музыки"; + public static final String MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_ID = "music_playlist_download_channel"; + public static final String MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_NAME = "Загрузка плейлиста"; + public static final String MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_DESCRIPTION = "Прогресс загрузки плейлиста"; + + public static final String MUSIC_CACHING_NAME = "Кеширование трека / Обложки"; + private static NotificationManagerCompat notificationManagerCompat; @RequiresApi(api = Build.VERSION_CODES.O) public static void createChannels() { createMusicDownloadChannel(); + createMusicPlaylistDownloadChannel(); } @RequiresApi(api = Build.VERSION_CODES.O) @@ -33,6 +39,16 @@ public class NotificationChannels { notificationManager.createNotificationChannel(musicDownloadChannel); } + @RequiresApi(api = Build.VERSION_CODES.O) + public static void createMusicPlaylistDownloadChannel() { + var musicDownloadChannel = new NotificationChannel(MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_ID, MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); + musicDownloadChannel.setDescription(MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_DESCRIPTION); + musicDownloadChannel.setShowBadge(true); + musicDownloadChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + var notificationManager = NotificationManagerCompat.from(AndroidUtils.getGlobalContext()); + notificationManager.createNotificationChannel(musicDownloadChannel); + } + public static NotificationManagerCompat getNotificationManager() { if (notificationManagerCompat == null) notificationManagerCompat = NotificationManagerCompat.from(AndroidUtils.getGlobalContext()); diff --git a/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java b/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java index fef07bbc0..fc2697079 100644 --- a/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java +++ b/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java @@ -6,7 +6,6 @@ import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.isVkxInstalle import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.lambdaplay; import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.isCached; import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.isVkxCached; -import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.removeTrackFromCache; import static ru.vtosters.lite.utils.NetworkUtils.*; import static ru.vtosters.lite.utils.Preferences.milkshake; @@ -22,7 +21,7 @@ import java.util.ArrayList; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl; -import ru.vtosters.lite.music.MP3Downloader; +import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.NetworkUtils; @@ -120,7 +119,7 @@ public class MusicBottomSheetHook { } if (actionId == AndroidUtils.getIdentifier("download_mp3", "id")) { - MP3Downloader.downloadMP3(track); + AudioDownloader.downloadAudio(track); AndroidUtils.sendToast("download_mp3"); return true; } @@ -143,7 +142,7 @@ public class MusicBottomSheetHook { } if (actionId == AndroidUtils.getIdentifier("download_mp3", "id")) { - MP3Downloader.downloadPlaylist(playlist); + AudioDownloader.downloadPlaylist(playlist); AndroidUtils.sendToast("download_mp3"); return true; } diff --git a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java index 00f4abc6a..b7b244754 100644 --- a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java @@ -1,5 +1,7 @@ package ru.vtosters.lite.music; +import com.vk.dto.music.MusicTrack; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -17,15 +19,21 @@ import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import ru.vtosters.lite.music.converter.ts.FFMpeg; +import ru.vtosters.lite.music.converter.ts.TSMerger; +import ru.vtosters.lite.music.interfaces.ITrackDownloader; import ru.vtosters.lite.utils.IOUtils; -public class M3UDownloader { +public class M3UDownloader implements ITrackDownloader { + private static final OkHttpClient client = new OkHttpClient(); - private static OkHttpClient client = new OkHttpClient(); + public static M3UDownloader getInstance() { + return Holder.INSTANCE; + } - public static void execute(String url, File outDir, Callback callback) { + public void downloadTrack(MusicTrack track, File outDir, Callback callback) { var request = new Request.a() - .b(url) + .b(track.D) .a(); client.a(request).a(new okhttp3.Callback() { @Override @@ -35,17 +43,22 @@ public class M3UDownloader { @Override public void a(Call call, Response response) throws IOException { - parse(response.a().g(), outDir, callback); + parse(response.a().g(), outDir, callback, track); } }); } - private static void parse(String payload, File outDir, Callback callback) { + private void parse(String payload, File outDir, Callback callback, MusicTrack track) { VKM3UParser parser = new VKM3UParser(payload); List tses = parser.getTransportStreams(); AtomicInteger progress = new AtomicInteger(0); List> futures = new ArrayList<>(); + var tsesDir = new File(outDir, String.valueOf(payload.hashCode())); + tsesDir.mkdirs(); + var resultTs = new File(tsesDir, "result.ts"); + var resultMp3 = new File(outDir, IOUtils.getValidFileName(track.toString()) + ".mp3"); + callback.onProgress(5); for (TransportStream ts : tses) { String tsURL = parser.getBaseUrl() + ts.getName(); @@ -61,20 +74,31 @@ public class M3UDownloader { } else { content = IOUtils.readAllBytes(is); } - File tsDump = new File(outDir, ts.getName()); + File tsDump = new File(tsesDir, ts.getName()); IOUtils.writeToFile(tsDump, content); callback.onSizeReceived((long) content.length * tses.size(), parser.getHeapSize()); } catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException e) { e.printStackTrace(); callback.onFailure(); } - }).thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size())))); - } - // wait for all futures - for (var future: futures) { - future.join(); + })); } - callback.onSuccess(); + var future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + future.thenApply(v -> TSMerger.merge(tsesDir, resultTs)) + .thenApply(mergeResult -> mergeResult && FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track)) + .thenApply(convertResult -> { + if (convertResult) + callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size())); + else callback.onFailure(); + return convertResult; + }) + .thenRun(() -> IOUtils.deleteRecursive(tsesDir)) + .thenRun(callback::onSuccess); + } + + // 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/MP3Downloader.java b/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java index ddff202ed..1d28adf35 100644 --- a/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java @@ -1,194 +1,76 @@ -package ru.vtosters.lite.music; - -import android.app.DownloadManager; -import android.app.Notification; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; - -import com.vk.dto.music.MusicTrack; -import com.vk.dto.music.Playlist; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.URL; -import java.util.stream.Collectors; - -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.downloaders.notifications.NotificationChannels; -import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; -import ru.vtosters.lite.music.cache.FileCacheImplementation; -import ru.vtosters.lite.utils.AndroidUtils; -import ru.vtosters.lite.utils.IOUtils; - -public class MP3Downloader { - - public final static int CACHE_STRATEGY = 0; - public final static int EXTERNAL_STORAGE_STRATEGY = 1; - - private static final String URL_FORMAT = "https://api.vk.com/method/audio.getById?v=5.131&access_token=%s&audios=%s©right=https://vtosters.app/"; - private static final OkHttpClient client = new OkHttpClient(); - - private static int getSelectedStrategy() { - return AndroidUtils.getPreferences().getInt("music_storing_strategy", CACHE_STRATEGY); - } - - public static void downloadPlaylist(Playlist playlist) { - var token = AndroidUtils.getPreferences().getString("vk_admin_token", ""); - if (token.isEmpty()) { - Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show(); - return; - } - - String trackIds = playlist.R - .stream() - .map(MusicTrack::y1) - .collect(Collectors.joining(",")); - - var request = new Request.a() - .b(String.format(URL_FORMAT, token, trackIds)) - .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; - var tracks = new JSONObject(payload).getJSONArray("response"); - for (int i = 0; i < tracks.length(); i++) { - JSONObject responseNode = tracks.getJSONObject(i); - if (!responseNode.has("url")) - continue; - var url = responseNode.optString("url"); - Log.d("MP3Downloader", url); - if (TextUtils.isEmpty(url)) - return; - var track = playlist.R.get(i); - switch (getSelectedStrategy()) { - case CACHE_STRATEGY: - saveToCache(track, url); - break; - case EXTERNAL_STORAGE_STRATEGY: - saveToDownloads(track.toString(), url); - } - } - } catch (JSONException e) { - Log.d("MP3Downloader", e + ""); - } - } - }); - } - - 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; - } - switch (getSelectedStrategy()) { - case CACHE_STRATEGY: - ArtworkDownloader.downloadArtworks(track); - saveToCache(track, url); - break; - case EXTERNAL_STORAGE_STRATEGY: - saveToDownloads(track.toString(), url); - break; - } - } catch (JSONException e) { - Log.d("MP3Downloader", e + ""); - } - } - }); - } - - private static void saveToCache(MusicTrack track, String url) throws IOException { - var trackId = track.y1(); - if (CacheDatabaseDelegate.isCached(trackId)) - return; - - var content = IOUtils.readAllBytes(new URL(url).openStream()); - var trackFile = FileCacheImplementation.getTrackFile(trackId); - if (!trackFile.exists()) - trackFile.getParentFile().mkdirs(); - IOUtils.writeToFile(trackFile, content); - notifySavingToCache(track); - CacheDatabaseDelegate.insertTrack(track); - NotificationChannels.getNotificationManager().cancel(trackId.hashCode()); - } - - private static void notifySavingToCache(MusicTrack track) { - Notification.Builder builder; - - if (Build.VERSION.SDK_INT >= 26) { - builder = new Notification.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_DOWNLOAD_CHANNEL_ID); - builder.setVisibility(Notification.VISIBILITY_PUBLIC); - } else builder = new Notification.Builder(AndroidUtils.getGlobalContext()); - - builder.setSmallIcon(com.vtosters.lite.R.drawable.ic_download_outline_28) - .setShowWhen(false) - .setContentTitle("Закешированный трек: " + track.toString()); - - NotificationChannels.getNotificationManager().notify(track.y1().hashCode(), builder.build()); - } - - private static void saveToDownloads(String title, String url) { - var downloadManager = AndroidUtils.getGlobalContext().getSystemService(DownloadManager.class); - var request = new DownloadManager.Request(Uri.parse(url)) - .setAllowedOverRoaming(true) - .setTitle("Downloading " + title) - .setDestinationInExternalFilesDir(AndroidUtils.getGlobalContext(), - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(), - title + ".mp3") - .setVisibleInDownloadsUi(true); - downloadManager.enqueue(request); - } -} +//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.Callback callback) { +// +// } +//} diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java b/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java index 5dba00286..dadeab312 100644 --- a/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java +++ b/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java @@ -17,7 +17,7 @@ import java.util.List; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import io.reactivex.Observable; import io.reactivex.ObservableOnSubscribe; -import ru.vtosters.lite.music.MP3Downloader; +import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.music.cache.helpers.PlaylistHelper; import ru.vtosters.lite.music.cache.helpers.TracklistHelper; @@ -28,7 +28,7 @@ public class PlaylistInjector { public final static String CHANNEL_NAME = "VTCH"; public static void injectDownloadPlaylist(Playlist playlist) { - MP3Downloader.downloadPlaylist(playlist); + AudioDownloader.downloadPlaylist(playlist); } public static boolean eligibleForOfflineCaching() { diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java b/app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java new file mode 100644 index 000000000..bc479d3f1 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java @@ -0,0 +1,76 @@ +package ru.vtosters.lite.music.converter.playlist; + +import static ru.vtosters.lite.hooks.DateHook.getLocale; +import static ru.vtosters.lite.proxy.ProxyUtils.getApi; + +import com.vk.core.network.Network; +import com.vk.core.util.DeviceIdProvider; +import com.vk.dto.music.MusicTrack; +import com.vk.dto.music.Playlist; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import java8.util.concurrent.CompletableFuture; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import ru.vtosters.lite.utils.AccountManagerUtils; +import ru.vtosters.lite.utils.AndroidUtils; + +/** + * stuff for converting playlists DTO + */ +public class PlaylistConverter { + public static List getPlaylist(Playlist oldPlaylist) { + var accessKey = oldPlaylist.Q; + + String requestUrl = "https://" + getApi() + "/method/execute.getPlaylist" + + "?v=5.119" + + "&https=1" + + "&audio_count=1000" + + "&need_playlist=1" + + "&device_id=" + DeviceIdProvider.d(AndroidUtils.getGlobalContext()) + + "&owner_id=" + oldPlaylist.b + + "&access_key=" + accessKey + + "&func_v=5" + + "&id=" + oldPlaylist.a + + "&lang=" + getLocale() + + "&audio_offset=0" + + "&need_owner=1" + + "&access_token=" + AccountManagerUtils.getUserToken(); + + var request = new Request.a() + .b(requestUrl) + .a(Headers.a("User-Agent", Network.l.c().a(), "Content-Type", "application/x-www-form-urlencoded; charset=utf-8")).a(); + + try { + var response = CompletableFuture.supplyAsync(() -> { + try { + return new OkHttpClient().a(request).execute().a().g(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ).get(); + var jsonObj = new JSONObject(response); + if (!jsonObj.has("response")) + return null; + var playlist = new JSONObject(response).getJSONObject("response").getJSONArray("audios"); + var tracks = new ArrayList(); + for (int i = 0; i < playlist.length(); i++) { + tracks.add(new MusicTrack(playlist.getJSONObject(i))); + } + + return tracks; + } catch (JSONException | ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/FFMpeg.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java similarity index 77% rename from app/src/main/java/ru/vtosters/lite/music/converter/FFMpeg.java rename to app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java index d71075d9e..08477e1ad 100644 --- a/app/src/main/java/ru/vtosters/lite/music/converter/FFMpeg.java +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java @@ -1,22 +1,67 @@ -package ru.vtosters.lite.music.converter; +package ru.vtosters.lite.music.converter.ts; import android.util.Log; import com.arthenica.ffmpegkit.FFmpegKit; import com.arthenica.ffmpegkit.ReturnCode; import com.vk.core.util.ToastUtils; +import com.vk.dto.music.Artist; import com.vk.dto.music.MusicTrack; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.stream.Collectors; import ru.vtosters.lite.utils.AndroidUtils; -import ru.vtosters.lite.utils.IOUtils; public class FFMpeg { private static final int BUFFER_SIZE = 2048; + /** + * @param in Merged .ts file via {@link ru.vtosters.lite.music.converter.ts.TSMerger} + * @param out Output .mp3 file + * @param track Track to be converted + * @return True if success, else false + */ + public static boolean convert(File in, String out, MusicTrack track) { + checkFFMpegLibs(); + + + // convert .ts file to .mp3 file via ffmpeg + var sb = new StringBuilder(); + sb.append("-y -i '").append(in.getAbsolutePath()).append("'"); + sb.append(" -map 0 -dn "); + sb.append(" -loglevel error"); + sb.append(" -hide_banner"); + sb.append(" -write_id3v2 1"); + if (track.f != null) { + sb.append(" -metadata title='").append(track.f).append("'"); + } + + if (track.L != null) { + sb.append(" -metadata artist='").append(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))).append("'"); + } else if (track.C != null) { + sb.append(" -metadata artist='").append(track.C).append("'"); + } + if (track.I != null) { + sb.append(" -metadata album='").append(track.I.getTitle()).append("'"); + } + + sb.append(" -c copy '").append(out).append("'"); + + var command = sb.toString(); + + var session = FFmpegKit.execute(command); + var rc = session.getReturnCode(); + + if (ReturnCode.isSuccess(rc)) { + return true; + } + + Log.e("FFMpeg", "FFmpegKit failed with return code " + rc); + return false; + } private static void copyAssets() { try { @@ -46,57 +91,4 @@ public class FFMpeg { copyAssets(); } } - - /** - * @param in Directory with .ts files - * @param out Output .mp3 file - * @param track Track to be converted - * @return True if success, else false - */ - public static boolean convert(String in, String out, MusicTrack track) { - checkFFMpegLibs(); - - var tsesDir = new File(in); - var mergedTsFile = new File(tsesDir, "all.ts"); - - var mergeResult = TSMerger.merge(tsesDir, mergedTsFile); - if (!mergeResult) return false; - - - // convert .ts file to .mp3 file via ffmpeg - var sb = new StringBuilder(); - sb.append("-y -i ").append(mergedTsFile.getAbsolutePath()); - sb.append(" -map 0 -dn "); - sb.append(" -loglevel error"); - sb.append(" -hide_banner"); - if (track.f != null) { - sb.append(" -metadata title='").append(track.f).append("'"); - } - if (track.C != null) { - sb.append(" -metadata artist='").append(track.C).append("'"); - } - if (track.I != null) { - sb.append(" -metadata album='").append(track.I.getTitle()).append("'"); - } -// -// if (track.L != null) { -// var artists = track.L.stream().map(Artist::w1).reduce((s, s2) -> s + ", " + s2).orElse(""); -// sb.append(" -metadata artist='").append(artists).append("'"); -// } - - sb.append(" -c copy '").append(out).append("'"); - - var command = sb.toString(); - - var session = FFmpegKit.execute(command); - var rc = session.getReturnCode(); - - IOUtils.deleteRecursive(new File(in)); - if (ReturnCode.isSuccess(rc)) { - return true; - } - - Log.e("FFMpeg", "FFmpegKit failed with return code " + rc); - return false; - } } diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/TSMerger.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java similarity index 97% rename from app/src/main/java/ru/vtosters/lite/music/converter/TSMerger.java rename to app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java index b8d292393..958441110 100644 --- a/app/src/main/java/ru/vtosters/lite/music/converter/TSMerger.java +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java @@ -1,4 +1,4 @@ -package ru.vtosters.lite.music.converter; +package ru.vtosters.lite.music.converter.ts; import android.os.Build; 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 new file mode 100644 index 000000000..2a6401c51 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java @@ -0,0 +1,80 @@ +package ru.vtosters.lite.music.downloader; + +import android.os.Environment; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.vk.dto.music.MusicTrack; + +import java.io.File; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import ru.vtosters.lite.downloaders.notifications.NotificationChannels; +import ru.vtosters.lite.music.Callback; +import ru.vtosters.lite.utils.AndroidUtils; + +public class PlaylistDownloader { + private static NotificationManagerCompat notificationManager = NotificationChannels.getNotificationManager(); + + public static void downloadPlaylist(List playlist, String playlistName) { + var musicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath(); + var downloadPath = musicPath + File.separator + playlistName; + + File outDir = new File(downloadPath); + if (!outDir.exists()) + if (outDir.mkdirs()) + Log.v("TrackDownloader", "Directory created"); + else + Log.e("TrackDownloader", "Directory creation failed"); + + var notificationId = playlistName.hashCode(); + var notification = buildDownloadNotification(playlistName, notificationId); + + var downloadedTracks = new AtomicInteger(0); + var tracksWithUrls = playlist.stream().filter(track -> !track.D.isEmpty()).collect(Collectors.toList()); + + for (MusicTrack musicTrack : tracksWithUrls) { + TrackDownloader.downloadTrack(musicTrack, downloadPath, new Callback() { + @Override + public void onProgress(int progress) {} + + @Override + public void onSuccess() { + var currentProgress = downloadedTracks.incrementAndGet(); + + if (currentProgress == tracksWithUrls.size()) { + notification.setContentText("Download complete"); + notification.setProgress(0, 0, false); + } else { + notification.setContentText("Downloaded " + currentProgress + " of " + tracksWithUrls.size() + "tracks"); + notification.setProgress(tracksWithUrls.size(), currentProgress, false); + } + notificationManager.notify(notificationId, notification.build()); + } + + @Override + public void onFailure() {} + + @Override + public void onSizeReceived(long size, long header) {} + }); + } + } + + private static NotificationCompat.Builder buildDownloadNotification(String playlistName, int id) { + var notificationBuilder = new NotificationCompat.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle("Загрузка плейлиста " + '«' + playlistName + '»') + .setContentText(playlistName) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setAutoCancel(true) + .setProgress(100, 0, false); + notificationManager.notify(id, notificationBuilder.build()); + return notificationBuilder; + } +} \ 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 new file mode 100644 index 000000000..b984ee164 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java @@ -0,0 +1,27 @@ +package ru.vtosters.lite.music.downloader; + +import android.util.Log; + +import com.vk.dto.music.MusicTrack; + +import java.io.File; + +import ru.vtosters.lite.music.Callback; +import ru.vtosters.lite.music.M3UDownloader; +import ru.vtosters.lite.music.interfaces.ITrackDownloader; + +public class TrackDownloader { + public static void downloadTrack(MusicTrack track, String path, Callback callback) { + download(track, path, callback, M3UDownloader.getInstance()); + } + + private static void download(MusicTrack track, String path, Callback callback, ITrackDownloader downloader) { + File outDir = new File(path); + if (!outDir.exists()) + if (outDir.mkdir()) + Log.v("TrackDownloader", "Directory created"); + else + Log.e("TrackDownloader", "Directory creation failed"); + downloader.downloadTrack(track, outDir, callback); + } +} 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 new file mode 100644 index 000000000..e61505674 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java @@ -0,0 +1,20 @@ +package ru.vtosters.lite.music.interfaces; + +import com.vk.dto.music.MusicTrack; + +import java.io.File; + +import ru.vtosters.lite.music.Callback; + +/** + * Interface for track downloaders + */ +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); +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java b/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java index d736f16e0..7894817fd 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java @@ -117,4 +117,8 @@ public class IOUtils { return size; } + + public static String getValidFileName(String fileName) { + return fileName.replaceAll("[\\\\/:*?\"<>|]", ""); + } }