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("[\\\\/:*?\"<>|]", "");
+ }
}