[music] The refactoring

one class should only do a one thing and do it great
This commit is contained in:
aliveoutside
2022-09-17 18:45:22 +03:00
parent db1b95b15a
commit 78d30f9546
13 changed files with 457 additions and 315 deletions
@@ -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
* <p>
* 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<MusicTrack> 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;
}
}
@@ -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());
@@ -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;
}
@@ -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<TransportStream> tses = parser.getTransportStreams();
AtomicInteger progress = new AtomicInteger(0);
List<CompletableFuture<Void>> 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();
}
}
@@ -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&copyright=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&copyright=";
//
// private static final OkHttpClient client = new OkHttpClient();
//
// public static void downloadMP3(MusicTrack track) {
// var token = AndroidUtils.getPreferences().getString("vk_admin_token", "");
// if (token.isEmpty()) {
// Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show();
// return;
// }
//
// var request = new Request.a()
// .b(String.format(URL_FORMAT, token, track.y1()))
// .a();
// client.a(request).a(new Callback() {
// @Override
// public void a(Call call, IOException e) {
// Log.d("MP3Downloader", e + "");
// }
//
// @Override
// public void a(Call call, Response response) throws IOException {
// var payload = response.a().g();
// Log.d("MP3Downloader", payload);
// try {
// JSONObject jsonObj = new JSONObject(payload);
// if (!jsonObj.has("response"))
// return;
// JSONObject responseNode = new JSONObject(payload).getJSONArray("response")
// .getJSONObject(0);
// if (!responseNode.has("url"))
// return;
// var url = responseNode.optString("url");
// Log.d("MP3Downloader", url);
// if (TextUtils.isEmpty(url))
// return;
// if (VKM3UParser.isM3U8(url)) {
// AudioDownloader.downloadM3U8(track);
// return;
// }
// } catch (JSONException e) {
// Log.d("MP3Downloader", e + "");
// }
// }
// });
// }
//
// @Override
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.Callback callback) {
//
// }
//}
@@ -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() {
@@ -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<MusicTrack> 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<MusicTrack>();
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;
}
}
@@ -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;
}
}
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.converter;
package ru.vtosters.lite.music.converter.ts;
import android.os.Build;
@@ -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<MusicTrack> 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;
}
}
@@ -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);
}
}
@@ -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);
}
@@ -117,4 +117,8 @@ public class IOUtils {
return size;
}
public static String getValidFileName(String fileName) {
return fileName.replaceAll("[\\\\/:*?\"<>|]", "");
}
}