[music] The refactoring
one class should only do a one thing and do it great
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+17
-1
@@ -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©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) {
|
||||
//
|
||||
// }
|
||||
//}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+47
-55
@@ -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
-1
@@ -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("[\\\\/:*?\"<>|]", "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user