transfer audio download to ffmpeg
This commit is contained in:
@@ -96,6 +96,7 @@ dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
implementation 'net.sourceforge.streamsupport:streamsupport-minifuture:1.7.4'
|
||||
implementation 'com.google.guava:guava:23.3-android'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
Binary file not shown.
@@ -18,15 +18,12 @@ import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class PlaylistInjector {
|
||||
public final static String CHANNEL_NAME = "VTCH";
|
||||
private static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
public static void injectDownloadPlaylist(Playlist playlist) {
|
||||
executor.submit(() -> AudioDownloader.cachePlaylist(playlist));
|
||||
AudioDownloader.cachePlaylist(playlist);
|
||||
}
|
||||
|
||||
public static boolean eligibleForOfflineCaching() {
|
||||
|
||||
@@ -4,12 +4,8 @@ import com.vk.core.concurrent.VkExecutors;
|
||||
import io.reactivex.Scheduler;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
public class VTExecutors {
|
||||
public static ExecutorService getMusicDownloadExecutor() {
|
||||
return VTMusicDownloadExecutor.getInstance();
|
||||
}
|
||||
|
||||
public static ExecutorService getIoExecutor() {
|
||||
return VkExecutors.x.e();
|
||||
@@ -22,30 +18,4 @@ public class VTExecutors {
|
||||
public static Scheduler getSlowTasksScheduler() {
|
||||
return VkExecutors.x.q();
|
||||
}
|
||||
|
||||
static class VTMusicDownloadExecutor {
|
||||
private VTMusicDownloadExecutor() {
|
||||
}
|
||||
|
||||
public static ExecutorService getInstance() {
|
||||
return LazyHolder.INSTANCE;
|
||||
}
|
||||
|
||||
private static class LazyHolder {
|
||||
|
||||
private static final int PARALLELISM = clamp(
|
||||
(Runtime.getRuntime().availableProcessors() >> 1) - 1,
|
||||
1, 7);
|
||||
|
||||
private static final ExecutorService INSTANCE =
|
||||
new ForkJoinPool(PARALLELISM,
|
||||
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
||||
(t, e) -> {},
|
||||
/* FIFO */ true);
|
||||
|
||||
private static int clamp(int value, int min, int max) {
|
||||
return Math.min(max, Math.max(value, min));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.vk.core.util.ToastUtils;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.dto.music.Playlist;
|
||||
import com.vtosters.lite.R;
|
||||
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
import ru.vtosters.hooks.music.MusicCacheFilesHook;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
@@ -20,8 +22,7 @@ import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.Objects;
|
||||
|
||||
import static ru.vtosters.lite.utils.AndroidUtils.getString;
|
||||
|
||||
@@ -34,7 +35,7 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
|
||||
public class AudioDownloader {
|
||||
public static final String dlpath = Preferences.getBoolValue("dldir", false) ? Environment.DIRECTORY_DOWNLOADS : Environment.DIRECTORY_MUSIC;
|
||||
|
||||
public static void downloadPlaylist(Playlist playlist) {
|
||||
public static CompletableFuture<Void> downloadPlaylist(Playlist playlist) {
|
||||
var tracks = PlaylistConverter.getPlaylist(playlist);
|
||||
|
||||
var playlistName = IOUtils.getValidFileName(playlist.g);
|
||||
@@ -45,7 +46,7 @@ public class AudioDownloader {
|
||||
var notificationId = playlistName.hashCode();
|
||||
var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlistName, notificationId);
|
||||
|
||||
PlaylistDownloader.downloadPlaylist(
|
||||
return PlaylistDownloader.downloadPlaylist(
|
||||
tracks,
|
||||
IOUtils.getValidFileName(playlist.g),
|
||||
downloadPath,
|
||||
@@ -64,22 +65,21 @@ public class AudioDownloader {
|
||||
if (MusicCacheImpl.isCachedTrack(trackId)) {
|
||||
MusicCacheImpl.removeTrack(trackId);
|
||||
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
|
||||
return;
|
||||
} else {
|
||||
var trackFile = MusicCacheFilesHook.getTrackFile(trackId);
|
||||
if (!trackFile.exists())
|
||||
trackFile.getParentFile().mkdirs();
|
||||
downloadM3U8(track, true);
|
||||
}
|
||||
|
||||
var trackFile = MusicCacheFilesHook.getTrackFile(trackId);
|
||||
if (!trackFile.exists())
|
||||
trackFile.getParentFile().mkdirs();
|
||||
downloadM3U8(track, true);
|
||||
}
|
||||
|
||||
public static void cachePlaylist(Playlist playlist) {
|
||||
public static CompletableFuture<Void> cachePlaylist(Playlist playlist) {
|
||||
var tracks = PlaylistConverter.getPlaylist(playlist);
|
||||
|
||||
var notificationId = playlist.g.hashCode();
|
||||
var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlist.g, notificationId);
|
||||
|
||||
PlaylistDownloader.cachePlaylist(
|
||||
return PlaylistDownloader.cachePlaylist(
|
||||
tracks,
|
||||
MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId)
|
||||
);
|
||||
@@ -116,21 +116,21 @@ public class AudioDownloader {
|
||||
);
|
||||
}
|
||||
|
||||
private static void downloadM3U8(MusicTrack track, boolean cache) {
|
||||
if (track.D == null) {
|
||||
private static void
|
||||
downloadM3U8(MusicTrack track, boolean cache) {
|
||||
Objects.requireNonNull(track.D, () -> {
|
||||
ToastUtils.a(getString(R.string.link_audio_error));
|
||||
return;
|
||||
}
|
||||
|
||||
return "link must not be null";
|
||||
});
|
||||
var musicPath = Environment.getExternalStoragePublicDirectory(dlpath).getAbsolutePath();
|
||||
var tempId = track.d;
|
||||
var downloadPath = musicPath + File.separator;
|
||||
var notification = MusicNotificationBuilder.buildDownloadNotification(track, tempId);
|
||||
|
||||
if (cache) {
|
||||
TrackDownloader.cacheTrack(track, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification));
|
||||
} else {
|
||||
TrackDownloader.downloadTrack(track, downloadPath, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification));
|
||||
}
|
||||
var cb = MusicCallbackBuilder.buildOneTrackCallback(tempId, notification);
|
||||
if (cache)
|
||||
TrackDownloader.cacheTrack(track, cb);
|
||||
else
|
||||
TrackDownloader.downloadTrack(track, downloadPath, cb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package ru.vtosters.lite.music.cache;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.cache.db.MusicCacheDb;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ru.vtosters.lite.music.cache.db.MusicCacheDb;
|
||||
|
||||
public class MusicCacheDbDelegate {
|
||||
static synchronized private MusicCacheDb connectToDb(Context context) {
|
||||
synchronized (MusicCacheDb.class) {
|
||||
return new MusicCacheDb(context);
|
||||
}
|
||||
private static MusicCacheDb connectToDb(Context context) {
|
||||
return new MusicCacheDb(context);
|
||||
}
|
||||
|
||||
public static void addTrack(Context context,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package ru.vtosters.lite.music.converter.ts;
|
||||
|
||||
import com.arthenica.ffmpegkit.FFmpegKit;
|
||||
import com.vk.dto.music.Artist;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FFMpeg {
|
||||
|
||||
/**
|
||||
* @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 void convert(File in, String out, MusicTrack track) {
|
||||
// convert .ts file to .mp3 file via ffmpeg
|
||||
var cmd = " -y -i \"" + in.getAbsolutePath() + '"'
|
||||
+ " -map 0 -dn"
|
||||
+ " -loglevel error"
|
||||
+ " -hide_banner"
|
||||
+ " -write_id3v2 1"
|
||||
+ setMetadata(track)
|
||||
+ " -c copy \"" + out + '"';
|
||||
var ses = FFmpegKit.execute(cmd);
|
||||
if (ses.getReturnCode().isValueCancel()) throw new RuntimeException(ses.getLogsAsString());
|
||||
}
|
||||
|
||||
private static String setMetadata(MusicTrack track) {
|
||||
if (!Preferences.getBoolValue("setMetaData", false)) {
|
||||
return "";
|
||||
}
|
||||
return " -metadata title=\"" + IOUtils.getValidFileName(M3UDownloader.getTitle(track)) + '"'
|
||||
+ (track.L == null || track.I == null
|
||||
? " -metadata artist=\"" + normalizeMetadata(track.C) + '"'
|
||||
: " -metadata artist=\"" + normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))) + '"'
|
||||
+ " -metadata album=\"" + normalizeMetadata(track.I.getTitle()) + '"');
|
||||
}
|
||||
|
||||
private static String normalizeMetadata(String in) {
|
||||
return in.replaceAll("[\\\\/:*?\"<>|]", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
|
||||
public final class CachedDownloader implements ITrackDownloader {
|
||||
|
||||
private final ITrackDownloader origin;
|
||||
|
||||
public CachedDownloader(ITrackDownloader origin) {
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void downloadTrack(MusicTrack track, Callback callback) {
|
||||
var d = new Callback.CompletableFutureCallback(callback);
|
||||
origin.downloadTrack(track, d);
|
||||
d.thenAccept(result -> {
|
||||
try {
|
||||
ThumbnailDownloader.downloadThumbnails(track);
|
||||
MusicCacheImpl.addTrack(track);
|
||||
} catch (IOException e) {
|
||||
callback.onFailure();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.source.hls.playlist.e;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.f;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.f.a;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.converter.ts.FFMpeg;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class M3UDownloader implements ITrackDownloader {
|
||||
public static M3UDownloader getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private static void parse(File outDir, Callback callback, MusicTrack track, boolean cache) throws IOException {
|
||||
var parser = new com.google.android.exoplayer2.source.hls.playlist.h(e.a(track.D));
|
||||
var baseUri = track.D.substring(0, track.D.lastIndexOf("/") + 1);
|
||||
var segments = ((f) parser.a(Uri.parse(baseUri), IOUtils.openStream(track.D))).o;
|
||||
|
||||
callback.onProgress(5);
|
||||
var resultTs = getResultTsFile(track);
|
||||
var resultMp3 = getResultMp3File(outDir, cache, track);
|
||||
try {
|
||||
byte[] buff = downloadTs(baseUri, segments, callback);
|
||||
|
||||
IOUtils.writeToFile(resultTs, buff);
|
||||
|
||||
if (cache) {
|
||||
try {
|
||||
ThumbnailDownloader.downloadThumbnails(track);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to download thumbs", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("FFmpeg error", e);
|
||||
}
|
||||
int sz = segments.size();
|
||||
callback.onProgress(10 + Math.round(80.0F * (sz + 1F) / sz));
|
||||
if (cache) {
|
||||
try {
|
||||
MusicCacheImpl.addTrack(track);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.e("M3UDownloader", "Failed to download track", e.getCause());
|
||||
} finally {
|
||||
resultTs.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] downloadTs(String baseUri, List<a> segments, Callback callback) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
byte[] total = new byte[0];
|
||||
|
||||
int size = segments.size();
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
|
||||
int progress = 0;
|
||||
for (var segment : segments) {
|
||||
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
|
||||
if (!TextUtils.isEmpty(segment.g)) {
|
||||
var cipherKey = new SecretKeySpec(IOUtils.readFully(IOUtils.openStream(segment.g)), "AES");
|
||||
var cipherBytes = segment.h.getBytes();
|
||||
//if IV doesn't pass, it must be created manually
|
||||
if (cipherBytes.length != 16) cipherBytes = new byte[16];
|
||||
var cipherIv = new IvParameterSpec(cipherBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
|
||||
buff = cipher.doFinal(buff);
|
||||
}
|
||||
|
||||
// copy on write
|
||||
final int len = total.length, n = buff.length;
|
||||
byte[] newBytes = Arrays.copyOf(total, len + n);
|
||||
System.arraycopy(buff, 0, newBytes, len, n);
|
||||
total = newBytes;
|
||||
|
||||
callback.onProgress(10 + Math.round(80.0F * (++progress) / size));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
public static String getTitle(MusicTrack track) {
|
||||
return track.f + (!TextUtils.isEmpty(track.g) ? '(' + track.g + ')' : "");
|
||||
}
|
||||
|
||||
static File getTsesDir(MusicTrack track) {
|
||||
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
|
||||
var tsesDir = new File(filesDir, String.valueOf(track.d));
|
||||
tsesDir.mkdirs();
|
||||
return tsesDir;
|
||||
}
|
||||
|
||||
static File getResultTsFile(MusicTrack track) {
|
||||
return new File(getTsesDir(track), "result.ts");
|
||||
}
|
||||
|
||||
static File getResultMp3File(File outDir, boolean cache, MusicTrack track) {
|
||||
return new File(outDir, IOUtils.getValidFileName((cache ? "track" : track.C + " - " + getTitle(track)) + ".mp3"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadTrack(MusicTrack track, File outDir, Callback callback, boolean cache) {
|
||||
if (track.D.isEmpty()) {
|
||||
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
|
||||
callback.onFailure();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
parse(outDir, callback, track, cache);
|
||||
callback.onSuccess();
|
||||
} catch (IOException e) {
|
||||
callback.onFailure();
|
||||
Log.e("M3UDownloader", String.valueOf(e));
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialization-on-demand
|
||||
private static class Holder {
|
||||
private static final M3UDownloader INSTANCE = new M3UDownloader();
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//package ru.vtosters.lite.music;
|
||||
//
|
||||
//import android.text.TextUtils;
|
||||
//import android.util.Log;
|
||||
//import android.widget.Toast;
|
||||
//
|
||||
//import com.vk.dto.music.MusicTrack;
|
||||
//
|
||||
//import org.json.JSONException;
|
||||
//import org.json.JSONObject;
|
||||
//
|
||||
//import java.io.File;
|
||||
//import java.io.IOException;
|
||||
//
|
||||
//import okhttp3.Call;
|
||||
//import okhttp3.Callback;
|
||||
//import okhttp3.OkHttpClient;
|
||||
//import okhttp3.Request;
|
||||
//import okhttp3.Response;
|
||||
//import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
//import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
//import ru.vtosters.lite.utils.AndroidUtils;
|
||||
//
|
||||
//public class MP3Downloader implements ITrackDownloader {
|
||||
// private static final String URL_FORMAT = "https://api.vk.com/method/audio.getById?v=5.131&access_token=%s&audios=%s©right=";
|
||||
//
|
||||
// private static final OkHttpClient client = new OkHttpClient();
|
||||
//
|
||||
// public static void downloadMP3(MusicTrack track) {
|
||||
// var token = AndroidUtils.getPreferences().getString("vk_admin_token", "");
|
||||
// if (token.isEmpty()) {
|
||||
// Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var request = new Request.a()
|
||||
// .b(String.format(URL_FORMAT, token, track.y1()))
|
||||
// .a();
|
||||
// client.a(request).a(new Callback() {
|
||||
// @Override
|
||||
// public void a(Call call, IOException e) {
|
||||
// Log.d("MP3Downloader", e + "");
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void a(Call call, Response response) throws IOException {
|
||||
// var payload = response.a().g();
|
||||
// Log.d("MP3Downloader", payload);
|
||||
// try {
|
||||
// JSONObject jsonObj = new JSONObject(payload);
|
||||
// if (!jsonObj.has("response"))
|
||||
// return;
|
||||
// JSONObject responseNode = new JSONObject(payload).getJSONArray("response")
|
||||
// .getJSONObject(0);
|
||||
// if (!responseNode.has("url"))
|
||||
// return;
|
||||
// var url = responseNode.optString("url");
|
||||
// Log.d("MP3Downloader", url);
|
||||
// if (TextUtils.isEmpty(url))
|
||||
// return;
|
||||
// if (VKM3UParser.isM3U8(url)) {
|
||||
// AudioDownloader.downloadM3U8(track);
|
||||
// return;
|
||||
// }
|
||||
// } catch (JSONException e) {
|
||||
// Log.d("MP3Downloader", e + "");
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.interfaces.Callback callback) {
|
||||
//
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,75 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.arthenica.ffmpegkit.FFmpegKit;
|
||||
import com.vk.dto.music.Artist;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
public final class Mp3Downloader implements ITrackDownloader {
|
||||
private final File outDir;
|
||||
public Mp3Downloader(File outDir) {
|
||||
this.outDir = outDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadTrack(MusicTrack track, Callback callback) {
|
||||
String uri = track.D;
|
||||
if (Objects.requireNonNull(uri).isEmpty()) {
|
||||
String msg = "link error: " +
|
||||
track.y1() + ", title: " + Mp3Downloader.getTitle(track);
|
||||
Log.d("TrackDownloader", msg);
|
||||
throw new RuntimeException(msg);
|
||||
}
|
||||
|
||||
var cmd = " -y -http_persistent false -i \"" + uri + '"'
|
||||
+ " -map 0 -dn"
|
||||
+ " -loglevel error"
|
||||
+ " -hide_banner"
|
||||
+ " -write_id3v2 1"
|
||||
+ setMetadata(track)
|
||||
+ " -c copy \"" + outDir.delete() + '"';
|
||||
|
||||
// todo: make a normal progression system
|
||||
final AtomicInteger progress = new AtomicInteger();
|
||||
FFmpegKit.executeAsync(cmd, session -> {
|
||||
if (session.getReturnCode().isValueSuccess()) {
|
||||
callback.onProgress(100);
|
||||
callback.onSuccess();
|
||||
} else
|
||||
callback.onFailure();
|
||||
}, log -> callback.onProgress(Math.min(100, progress.incrementAndGet())),
|
||||
statistics -> {}
|
||||
);
|
||||
}
|
||||
|
||||
private static String setMetadata(MusicTrack track) {
|
||||
if (!Preferences.getBoolValue("setMetaData", false)) {
|
||||
return "";
|
||||
}
|
||||
return " -metadata title=\"" + IOUtils.getValidFileName(Mp3Downloader.getTitle(track)) + '"'
|
||||
+ (track.L == null || track.I == null
|
||||
? " -metadata artist=\"" + normalizeMetadata(track.C) + '"'
|
||||
: " -metadata artist=\"" + normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))) + '"'
|
||||
+ " -metadata album=\"" + normalizeMetadata(track.I.getTitle()) + '"');
|
||||
}
|
||||
|
||||
private static String normalizeMetadata(String in) {
|
||||
return in.replaceAll("[\\\\/:*?\"<>|]", "");
|
||||
}
|
||||
|
||||
private static String getTitle(MusicTrack track) {
|
||||
return track.f + (!TextUtils.isEmpty(track.g) ? '(' + track.g + ')' : "");
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,68 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
|
||||
public class PlaylistDownloader {
|
||||
public static void downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
|
||||
public static CompletableFuture<Void>
|
||||
downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
|
||||
var outDir = new File(path);
|
||||
if (!outDir.exists())
|
||||
if (outDir.mkdirs()) Log.v("PlaylistDownloader", "Directory created");
|
||||
else Log.e("PlaylistDownloader", "Directory creation failed");
|
||||
var downloadedTracks = new AtomicInteger();
|
||||
for (var musicTrack : playlist)
|
||||
TrackDownloader.downloadTrack(musicTrack, path, new Callback() {
|
||||
@Override
|
||||
public void onProgress(int progress) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
callback.onProgress(downloadedTracks.incrementAndGet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
callback.onFailure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSizeReceived(long size, long header) {
|
||||
}
|
||||
});
|
||||
Callback delegate = new ProgressCallback(callback);
|
||||
return CompletableFuture.allOf(playlist
|
||||
.stream()
|
||||
.map(x -> {
|
||||
Callback.CompletableFutureCallback d = new
|
||||
Callback.CompletableFutureCallback(delegate);
|
||||
TrackDownloader.downloadTrack(x, path, d);
|
||||
return d;
|
||||
})
|
||||
.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
public static void cachePlaylist(List<MusicTrack> playlist, Callback callback) {
|
||||
var downloadedTracks = new AtomicInteger();
|
||||
for (var musicTrack : playlist)
|
||||
TrackDownloader.cacheTrack(musicTrack, new Callback() {
|
||||
@Override
|
||||
public void onProgress(int progress) {
|
||||
}
|
||||
public static CompletableFuture<Void> cachePlaylist(List<MusicTrack> playlist, Callback callback) {
|
||||
Callback delegate = new ProgressCallback(callback);
|
||||
return CompletableFuture.allOf(playlist
|
||||
.stream()
|
||||
.map(x -> {
|
||||
Callback.CompletableFutureCallback d = new
|
||||
Callback.CompletableFutureCallback(delegate);
|
||||
TrackDownloader.cacheTrack(x, d);
|
||||
return d;
|
||||
})
|
||||
.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
callback.onProgress(downloadedTracks.incrementAndGet());
|
||||
}
|
||||
public static final class ProgressCallback implements Callback {
|
||||
private final AtomicInteger progress = new AtomicInteger();
|
||||
private final Callback origin;
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
callback.onFailure();
|
||||
}
|
||||
public ProgressCallback(Callback origin) {
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSizeReceived(long size, long header) {
|
||||
}
|
||||
});
|
||||
@Override public void onProgress(int progress) {}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
origin.onProgress(progress.incrementAndGet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
origin.onFailure();
|
||||
}
|
||||
|
||||
@Override public void onSizeReceived(long size, long header) {}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,41 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import android.util.Log;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.concurrent.VTExecutors;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class TrackDownloader {
|
||||
public static void downloadTrack(MusicTrack track, String path, Callback callback) {
|
||||
if (track.D.isEmpty()) {
|
||||
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
|
||||
callback.onFailure();
|
||||
return;
|
||||
}
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
download(track, path, callback, M3UDownloader.getInstance(), false);
|
||||
}
|
||||
public final class TrackDownloader {
|
||||
|
||||
public static void cacheTrack(MusicTrack track, Callback callback) {
|
||||
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) {
|
||||
callback.onSuccess();
|
||||
return;
|
||||
} else if (track.D.isEmpty()) {
|
||||
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
|
||||
callback.onFailure();
|
||||
return;
|
||||
}
|
||||
private TrackDownloader() { }
|
||||
|
||||
var path = MusicCacheStorageUtils.getTrackDirById(LibVKXClient.asId(track)).getAbsolutePath();
|
||||
download(track, path, callback, M3UDownloader.getInstance(), true);
|
||||
}
|
||||
|
||||
private static void download(MusicTrack track, String path, Callback callback, ITrackDownloader downloader, boolean cache) {
|
||||
public static void
|
||||
downloadTrack(MusicTrack track, String path, Callback callback) {
|
||||
File outDir = new File(path);
|
||||
if (!outDir.exists())
|
||||
if (!outDir.exists()) {
|
||||
if (outDir.mkdir())
|
||||
Log.v("TrackDownloader", "Directory created");
|
||||
else
|
||||
Log.e("TrackDownloader", "Directory creation failed");
|
||||
}
|
||||
new Mp3Downloader(outDir).downloadTrack(track, callback);
|
||||
}
|
||||
|
||||
VTExecutors.getMusicDownloadExecutor().execute(
|
||||
() -> downloader.downloadTrack(track, outDir, callback, cache)
|
||||
);
|
||||
public static void
|
||||
cacheTrack(MusicTrack track, Callback callback) {
|
||||
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track)))
|
||||
return;
|
||||
var path = MusicCacheStorageUtils
|
||||
.getTrackDirById(LibVKXClient.asId(track));
|
||||
|
||||
new CachedDownloader(
|
||||
new Mp3Downloader(path)
|
||||
).downloadTrack(track, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ru.vtosters.lite.music.interfaces;
|
||||
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Callback {
|
||||
void onProgress(int progress);
|
||||
|
||||
@@ -8,4 +10,37 @@ public interface Callback {
|
||||
void onFailure();
|
||||
|
||||
void onSizeReceived(long size, long header);
|
||||
|
||||
|
||||
class CompletableFutureCallback
|
||||
extends CompletableFuture<Void>
|
||||
implements Callback {
|
||||
private final Callback origin;
|
||||
|
||||
public CompletableFutureCallback(Callback origin) {
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(int progress) {
|
||||
origin.onProgress(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
complete(null);
|
||||
origin.onSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
completeExceptionally(new RuntimeException("callback failed"));
|
||||
origin.onFailure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSizeReceived(long size, long header) {
|
||||
origin.onSizeReceived(size, header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package ru.vtosters.lite.music.interfaces;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Interface for track downloaders
|
||||
*/
|
||||
@@ -12,8 +10,7 @@ public interface ITrackDownloader {
|
||||
* Download track
|
||||
*
|
||||
* @param track Music track
|
||||
* @param outDir Directory where to download the track
|
||||
* @param callback Callback to call when download status changes
|
||||
*/
|
||||
void downloadTrack(MusicTrack track, File outDir, Callback callback, boolean cache);
|
||||
void downloadTrack(MusicTrack track, Callback callback);
|
||||
}
|
||||
@@ -9,15 +9,21 @@ import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
|
||||
import com.vk.core.dialogs.alert.VkAlertDialog;
|
||||
import com.vk.core.network.Network;
|
||||
import com.vtosters.lite.R;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.hooks.other.ThemesUtils;
|
||||
import ru.vtosters.lite.concurrent.VTExecutors;
|
||||
@@ -32,13 +38,7 @@ import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.LifecycleUtils;
|
||||
import ru.vtosters.lite.utils.SearchEngine;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
|
||||
private static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
public static void download(Context ctx) {
|
||||
final EditText input = new EditText(ctx);
|
||||
input.setTextColor(ThemesUtils.getTextAttr());
|
||||
@@ -221,7 +221,9 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.cached_tracks_remove_confirm)
|
||||
.setPositiveButton(R.string.yes,
|
||||
(dialog, which) -> executor.submit(MusicCacheImpl::clear))
|
||||
(dialog, which) -> VTExecutors
|
||||
.getIoExecutor()
|
||||
.execute(MusicCacheImpl::clear))
|
||||
.setNeutralButton(R.string.no,
|
||||
(dialog, which) -> dialog.cancel())
|
||||
.show();
|
||||
@@ -237,10 +239,12 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
|
||||
.setTitle(R.string.download_method)
|
||||
.setMessage(R.string.download_method_desc)
|
||||
.setPositiveButton(R.string.download_method_cache, (dialog, which) -> {
|
||||
executor.submit(AudioDownloader::cacheAllAudios);
|
||||
VTExecutors
|
||||
.getIoExecutor()
|
||||
.execute(AudioDownloader::cacheAllAudios);
|
||||
})
|
||||
.setNegativeButton(R.string.download_method_mp3, (dialog, which) -> {
|
||||
executor.submit(AudioDownloader::downloadAllAudios);
|
||||
AudioDownloader.downloadAllAudios();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user