Merge remote-tracking branch 'sunmisc/main' #1554
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package ru.vtosters.lite.concurrent;
|
||||
|
||||
import com.vk.core.concurrent.VkExecutors;
|
||||
import io.reactivex.Scheduler;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
import io.reactivex.Scheduler;
|
||||
|
||||
public class VTExecutors {
|
||||
public static ExecutorService getMusicDownloadExecutor() {
|
||||
@@ -32,7 +33,21 @@ public class VTExecutors {
|
||||
}
|
||||
|
||||
private static class LazyHolder {
|
||||
public static final ExecutorService INSTANCE = Executors.newSingleThreadExecutor();
|
||||
private static final int MIN_PARALLELISM = 1;
|
||||
private static final int MAX_PARALLELISM = 7;
|
||||
private static final int PARALLELISM = clamp(
|
||||
(Runtime.getRuntime().availableProcessors() >> 1) - 1
|
||||
);
|
||||
|
||||
private static final ExecutorService INSTANCE =
|
||||
new ForkJoinPool(PARALLELISM,
|
||||
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
||||
(t, e) -> {},
|
||||
/* FIFO */ true);
|
||||
|
||||
private static int clamp(int value) {
|
||||
return Math.min(LazyHolder.MAX_PARALLELISM, Math.max(value, LazyHolder.MIN_PARALLELISM));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,10 +33,9 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString;
|
||||
*/
|
||||
|
||||
public class AudioDownloader {
|
||||
public static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
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);
|
||||
@@ -46,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,
|
||||
@@ -65,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();
|
||||
executor.submit(() -> 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)
|
||||
);
|
||||
@@ -117,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,25 @@ package ru.vtosters.lite.music;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vtosters.lite.R;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.sponsorpost.utils.GzipDecompressor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicTrackUtils;
|
||||
import ru.vtosters.sponsorpost.utils.GzipDecompressor;
|
||||
|
||||
public class Genius {
|
||||
private static final String KEY = "Bearer ZTejoT_ojOEasIkT9WrMBhBQOz6eYKK5QULCMECmOhvwqjRZ6WbpamFe3geHnvp3";
|
||||
private static final String URL = "https://api.genius.com";
|
||||
@@ -25,7 +28,7 @@ public class Genius {
|
||||
|
||||
public static String getTextMusic(MusicTrack musictrack) {
|
||||
String uid = musictrack.y1();
|
||||
String artist = removeExtras(M3UDownloader.getArtists(musictrack));
|
||||
String artist = removeExtras(MusicTrackUtils.getArtists(musictrack));
|
||||
String title = removeExtras(musictrack.f);
|
||||
int duration = musictrack.h;
|
||||
|
||||
|
||||
@@ -3,19 +3,14 @@ package ru.vtosters.lite.music;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vtosters.lite.R;
|
||||
import okhttp3.*;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -24,6 +19,20 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
|
||||
import ru.vtosters.lite.downloaders.AudioDownloader;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicTrackUtils;
|
||||
|
||||
public class LastFMScrobbler {
|
||||
private static final String KEY = "5965d63402414776c54c266db0211746";
|
||||
private static final String KEY_SECRET = "d982180eed13275bb948e41cf225f88f";
|
||||
@@ -33,7 +42,7 @@ public class LastFMScrobbler {
|
||||
|
||||
public static void grabMusicTrack(MusicTrack musictrack) {
|
||||
var uid = musictrack.y1();
|
||||
var artist = M3UDownloader.getArtists(musictrack);
|
||||
var artist = MusicTrackUtils.getArtists(musictrack);
|
||||
var title = musictrack.f;
|
||||
var duration = musictrack.h;
|
||||
var track_id = musictrack.d;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,15 +2,17 @@ package ru.vtosters.lite.music.cache;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.util.List;
|
||||
import ru.vtosters.lite.utils.music.MusicTrackUtils;
|
||||
|
||||
public class MusicCacheImpl {
|
||||
public static void addTrack(MusicTrack track) {
|
||||
@@ -20,7 +22,7 @@ public class MusicCacheImpl {
|
||||
track.I != null ? track.I.getId() + "" : "-1",
|
||||
track.f,
|
||||
!TextUtils.isEmpty(track.g) ? track.g : "",
|
||||
M3UDownloader.getArtists(track),
|
||||
MusicTrackUtils.getArtists(track),
|
||||
track.I != null ? track.I.getTitle() : "",
|
||||
track.K,
|
||||
track.h,
|
||||
|
||||
@@ -8,14 +8,14 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.vk.dto.music.AlbumLink;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.dto.music.Thumb;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.annotation.Retention;
|
||||
@@ -24,6 +24,10 @@ import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicTrackUtils;
|
||||
|
||||
/**
|
||||
* <p class="note"><strong>Note:</strong> the {@link AutoCloseable} interface was
|
||||
* first added in the {@link android.os.Build.VERSION_CODES#Q} release for the {@link SQLiteOpenHelper} </p>
|
||||
@@ -117,7 +121,7 @@ public class MusicCacheDb extends SQLiteOpenHelper implements AutoCloseable { //
|
||||
vals.put(Constants.COLUMN_ALBUM_ID, track.I != null ? track.I.getId() + "" : "-1");
|
||||
vals.put(Constants.COLUMN_TITLE, track.f);
|
||||
vals.put(Constants.COLUMN_SUBTITLE, track.g);
|
||||
vals.put(Constants.COLUMN_ARTIST, M3UDownloader.getArtists(track));
|
||||
vals.put(Constants.COLUMN_ARTIST, MusicTrackUtils.getArtists(track));
|
||||
vals.put(Constants.COLUMN_ALBUM_TITLE, track.I != null ? track.I.getTitle() : "");
|
||||
vals.put(Constants.COLUMN_EXPLICIT, Boolean.compare(track.K, true));
|
||||
vals.put(Constants.COLUMN_DURATION, track.h);
|
||||
|
||||
+10
-7
@@ -1,20 +1,23 @@
|
||||
package ru.vtosters.lite.music.cache.helpers;
|
||||
|
||||
import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.vk.dto.music.Playlist;
|
||||
import com.vtosters.lite.R;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.downloader.M3UDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicTrackUtils;
|
||||
|
||||
public class PlaylistHelper {
|
||||
|
||||
@@ -34,7 +37,7 @@ public class PlaylistHelper {
|
||||
return createAlbum(
|
||||
thumb != null ? thumb.h(600) : "",
|
||||
albumLink.getTitle(),
|
||||
M3UDownloader.getArtists(track),
|
||||
MusicTrackUtils.getArtists(track),
|
||||
track.y1(),
|
||||
albumLink.getId()
|
||||
);
|
||||
@@ -76,7 +79,7 @@ public class PlaylistHelper {
|
||||
list.add(createAlbum(
|
||||
thumb != null ? thumb.h(600) : "",
|
||||
albumLink.getTitle(),
|
||||
M3UDownloader.getArtists(track),
|
||||
MusicTrackUtils.getArtists(track),
|
||||
track.y1(),
|
||||
albumLink.getId()
|
||||
));
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package ru.vtosters.lite.music.callback;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.vtosters.lite.R;
|
||||
|
||||
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
@@ -40,12 +43,12 @@ public class MusicCallbackBuilder {
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.e("AudioDownloader", "native libs");
|
||||
Log.e("AudioDownloader", e.getMessage());
|
||||
onFailure();
|
||||
onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
public void onFailure(Throwable e) {
|
||||
notification.setContentText(AndroidUtils.getString(R.string.load_audio_error)).setProgress(0, 0, false);
|
||||
notificationManager.notify(notificationId, notification.build());
|
||||
}
|
||||
@@ -78,7 +81,7 @@ public class MusicCallbackBuilder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
public void onFailure(Throwable e) {
|
||||
notification.setContentText(AndroidUtils.getString(R.string.load_audio_error)).setProgress(0, 0, false);
|
||||
notificationManager.notify(notificationId, notification.build());
|
||||
}
|
||||
|
||||
@@ -2,26 +2,30 @@ package ru.vtosters.lite.music.converter.ts;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class MpegDemuxer {
|
||||
public static void convert(File in, String out, MusicTrack track) throws IOException {
|
||||
public static void convert(byte[] inBytes, String out) throws IOException {
|
||||
try (InputStream inStream = new ByteArrayInputStream(inBytes)) {
|
||||
convert(inStream, out);
|
||||
}
|
||||
}
|
||||
|
||||
public static void convert(InputStream in, String out) throws IOException {
|
||||
int packetSize = 188;
|
||||
int desiredPid = 256;
|
||||
|
||||
var buffer = new byte[packetSize];
|
||||
var packet = ByteBuffer.wrap(buffer).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
try (var fis = new BufferedInputStream(new FileInputStream(in)); var fos = new BufferedOutputStream(new FileOutputStream(out))) {
|
||||
try (var fis = new BufferedInputStream(in); var fos = new BufferedOutputStream(new FileOutputStream(out))) {
|
||||
while (fis.read(buffer) != -1) {
|
||||
packet.clear();
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package ru.vtosters.lite.music.converter.ts;
|
||||
|
||||
import android.os.Build;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class TSMerger {
|
||||
/**
|
||||
* Merge multiple .ts files into one .ts file
|
||||
*
|
||||
* @param in Directory with .ts files
|
||||
*/
|
||||
public static void merge(File in, File out) {
|
||||
File[] tsesList = in.listFiles();
|
||||
|
||||
if (tsesList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream mergedByteArrayStream = new ByteArrayOutputStream();
|
||||
|
||||
sortTsesList(tsesList);
|
||||
|
||||
for (File file : tsesList) {
|
||||
if (file.getName().endsWith(".ts")) {
|
||||
try {
|
||||
mergedByteArrayStream.write(IOUtils.readFully(file));
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeOutputFile(out, mergedByteArrayStream);
|
||||
}
|
||||
|
||||
private static void sortTsesList(File[] tsesList) {
|
||||
Arrays.sort(tsesList, Comparator.comparingInt(TSMerger::getFileIndex));
|
||||
}
|
||||
|
||||
private static int getFileIndex(File file) {
|
||||
String fileName = file.getName();
|
||||
int dotIndex = fileName.indexOf('.', 1);
|
||||
return Integer.parseInt(fileName.substring(0, dotIndex));
|
||||
}
|
||||
|
||||
private static void writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
|
||||
byte[] bytes = mergedByteArrayStream.toByteArray();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
Files.write(out.toPath(), bytes);
|
||||
} catch (IOException ignored) {
|
||||
// ignored
|
||||
}
|
||||
} else {
|
||||
try (FileOutputStream fos = new FileOutputStream(out)) {
|
||||
fos.write(bytes);
|
||||
} catch (IOException ignored) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
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 download(MusicTrack track, Callback callback) {
|
||||
origin.download(track, new Callback() {
|
||||
@Override
|
||||
public void onProgress(int progress) {
|
||||
callback.onProgress(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
new ThumbnailDownloader().download(track, new Callback() {
|
||||
@Override public void onProgress(int progress) {}
|
||||
|
||||
@Override public void onSuccess() {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
|
||||
@Override public void onSizeReceived(long size, long header) {}
|
||||
});
|
||||
MusicCacheImpl.addTrack(track);
|
||||
callback.onSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSizeReceived(long size, long header) {
|
||||
callback.onSizeReceived(size, header);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class ID3Tagger {
|
||||
public static void tag(File file, MusicTrack track) {
|
||||
try {
|
||||
// https://github.com/mpatric/mp3agic/issues/69#issuecomment-57897874
|
||||
// TODO: get rid of file duplication
|
||||
var oldFile = new File(file.getAbsolutePath().replace(".mp3", "_old.mp3"));
|
||||
if (!file.renameTo(oldFile)) {
|
||||
Log.e("ID3Tagger", "Failed to rename file");
|
||||
@@ -38,7 +39,7 @@ public class ID3Tagger {
|
||||
id3v2Tag.setAlbum(track.I.getTitle());
|
||||
}
|
||||
|
||||
id3v2Tag.setTitle(M3UDownloader.getTitle(track));
|
||||
id3v2Tag.setTitle(Mp3Downloader.getTitle(track));
|
||||
|
||||
mp3File.setId3v2Tag(id3v2Tag);
|
||||
mp3File.save(file.getAbsolutePath());
|
||||
|
||||
@@ -1,199 +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.Artist;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
import java8.util.concurrent.CompletionException;
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.converter.ts.MpegDemuxer;
|
||||
import ru.vtosters.lite.music.converter.ts.TSMerger;
|
||||
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 ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class M3UDownloader
|
||||
implements ITrackDownloader {
|
||||
public static M3UDownloader getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
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;
|
||||
var progress = new AtomicInteger();
|
||||
callback.onProgress(5);
|
||||
var resultTs = getResultTsFile(track);
|
||||
var resultMp3 = getResultMp3File(outDir, cache, track);
|
||||
try {
|
||||
CompletableFuture.allOf(createFutures(baseUri, segments, progress, callback, track))
|
||||
.thenRun(() -> {
|
||||
if (cache) {
|
||||
try {
|
||||
ThumbnailDownloader.downloadThumbnails(track);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to download thumbs", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
try {
|
||||
TSMerger.merge(getTsesDir(track), resultTs);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Failed to merge ts files", e);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
try {
|
||||
MpegDemuxer.convert(resultTs, resultMp3.getAbsolutePath(), track);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("MpegDemuxer error", e);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size())))
|
||||
.thenRun(() -> {
|
||||
if (Preferences.getBoolValue("setMetaData", true)) {
|
||||
ID3Tagger.tag(resultMp3, track);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
if (cache) {
|
||||
MusicCacheImpl.addTrack(track);
|
||||
}
|
||||
})
|
||||
.whenComplete((unused, tr) -> {
|
||||
if (tr != null) {
|
||||
IOUtils.deleteRecursive(MusicCacheStorageUtils.getTrackDirById(track.y1()));
|
||||
callback.onFailure();
|
||||
} else {
|
||||
callback.onSuccess();
|
||||
}
|
||||
IOUtils.deleteRecursive(getTsesDir(track));
|
||||
})
|
||||
.join();
|
||||
} catch (CompletionException e) {
|
||||
Log.e("M3UDownloader", "Failed to download track", e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
static CompletableFuture[] createFutures(
|
||||
String baseUri,
|
||||
List<a> segments,
|
||||
AtomicInteger progress,
|
||||
Callback callback,
|
||||
MusicTrack track) {
|
||||
var futures = new CompletableFuture[segments.size()];
|
||||
for (var i = 0; i < segments.size(); ++i) {
|
||||
var segment = segments.get(i);
|
||||
var filename = i + ".ts";
|
||||
futures[i] = CompletableFuture.runAsync(() -> processSegment(baseUri, segment, filename, progress, segments.size(), callback, track));
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
|
||||
private static void processSegment(
|
||||
String baseUri,
|
||||
a segment,
|
||||
String filename,
|
||||
AtomicInteger progress,
|
||||
int size,
|
||||
Callback callback,
|
||||
MusicTrack track) {
|
||||
try {
|
||||
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
|
||||
if (!TextUtils.isEmpty(segment.g)) {
|
||||
var cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
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 and filled with zeros
|
||||
if (cipherBytes.length != 16) Arrays.fill((cipherBytes = new byte[16]), (byte) 0x0);
|
||||
var cipherIv = new IvParameterSpec(cipherBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
|
||||
buff = cipher.doFinal(buff);
|
||||
}
|
||||
IOUtils.writeToFile(new File(getTsesDir(track), filename), buff);
|
||||
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
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" : getArtists(track) + " - " + 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);
|
||||
} catch (IOException e) {
|
||||
callback.onFailure();
|
||||
Log.e("M3UDownloader", e + "");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialization-on-demand
|
||||
private static class Holder {
|
||||
private static final M3UDownloader INSTANCE = new M3UDownloader();
|
||||
}
|
||||
|
||||
public static String getArtists(MusicTrack track) {
|
||||
return (track.L == null || track.I == null
|
||||
? normalizeMetadata(track.C)
|
||||
: normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))));
|
||||
}
|
||||
|
||||
private static String normalizeMetadata(String in) {
|
||||
return in.replaceAll("[\\\\/:*?\"<>|]", "");
|
||||
}
|
||||
}
|
||||
@@ -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,97 @@
|
||||
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 java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import ru.vtosters.hooks.other.Preferences;
|
||||
import ru.vtosters.lite.music.converter.ts.MpegDemuxer;
|
||||
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 outputFile;
|
||||
|
||||
public Mp3Downloader(File outputFile) {
|
||||
this.outputFile = outputFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(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("Mp3Downloader", msg);
|
||||
throw new RuntimeException(msg);
|
||||
}
|
||||
|
||||
var tsParser = new com.google.android.exoplayer2.source.hls.playlist.h(e.a(track.D));
|
||||
var baseUri = track.D.substring(0, track.D.lastIndexOf("/") + 1);
|
||||
|
||||
try {
|
||||
var segments = ((f) tsParser.a(Uri.parse(baseUri), IOUtils.openStream(track.D))).o;
|
||||
|
||||
byte[] buff = getMergedTs(baseUri, segments, callback);
|
||||
MpegDemuxer.convert(buff, outputFile.getAbsolutePath());
|
||||
|
||||
if (Preferences.getBoolValue("setMetaData", false)) {
|
||||
ID3Tagger.tag(outputFile, track);
|
||||
}
|
||||
|
||||
callback.onSuccess();
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getMergedTs(String baseUri, List<a> segments, Callback callback) throws IOException, GeneralSecurityException {
|
||||
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;
|
||||
}
|
||||
|
||||
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(Throwable e) {
|
||||
origin.onFailure(e);
|
||||
}
|
||||
|
||||
@Override public void onSizeReceived(long size, long header) {}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
package ru.vtosters.lite.music.downloader;
|
||||
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
public class ThumbnailDownloader {
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
public static void downloadThumbnails(MusicTrack track) throws IOException {
|
||||
public final class ThumbnailDownloader implements ITrackDownloader {
|
||||
|
||||
@Override
|
||||
public void download(MusicTrack track, Callback callback) {
|
||||
JSONObject json = track.J();
|
||||
JSONObject album = json.optJSONObject("album");
|
||||
if (album == null) return;
|
||||
@@ -20,24 +25,30 @@ public class ThumbnailDownloader {
|
||||
if (thumb == null) return;
|
||||
JSONArray sizes = thumb.optJSONArray("sizes");
|
||||
String trackId = LibVKXClient.asId(track);
|
||||
if (sizes == null) {
|
||||
JSONArray names = thumb.names();
|
||||
if (names == null) return;
|
||||
for (int i = 0; i < names.length(); ++i) {
|
||||
String name = names.optString(i);
|
||||
downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < sizes.length(); ++i) {
|
||||
JSONObject size = sizes.optJSONObject(i);
|
||||
int width = size.optInt("width");
|
||||
String src = size.optString("src");
|
||||
downloadThumbnail(src, width, trackId);
|
||||
try {
|
||||
if (sizes == null) {
|
||||
JSONArray names = thumb.names();
|
||||
if (names == null) return;
|
||||
for (int i = 0; i < names.length(); ++i) {
|
||||
String name = names.optString(i);
|
||||
downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < sizes.length(); ++i) {
|
||||
JSONObject size = sizes.optJSONObject(i);
|
||||
int width = size.optInt("width");
|
||||
String src = size.optString("src");
|
||||
downloadThumbnail(src, width, trackId);
|
||||
}
|
||||
}
|
||||
callback.onSuccess();
|
||||
} catch (IOException e) {
|
||||
callback.onFailure(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadThumbnail(String url, int res, String trackId) throws IOException {
|
||||
IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId, res), IOUtils.readFully(new URL(url).openStream()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
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 {
|
||||
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
|
||||
import ru.vtosters.lite.concurrent.VTExecutors;
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicTrackUtils;
|
||||
|
||||
public final class TrackDownloader {
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
download(track, path, callback, M3UDownloader.getInstance(), false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
var outputFile = new File(outDir, IOUtils.getValidFileName(
|
||||
MusicTrackUtils.getArtists(track) + " - " + Mp3Downloader.getTitle(track)) + ".mp3");
|
||||
|
||||
VTExecutors.getMusicDownloadExecutor().execute(
|
||||
() -> downloader.downloadTrack(track, outDir, callback, cache)
|
||||
);
|
||||
VTExecutors.getMusicDownloadExecutor().submit(() -> new Mp3Downloader(outputFile).download(track, callback));
|
||||
}
|
||||
|
||||
public static void cacheTrack(MusicTrack track, Callback callback) {
|
||||
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track)))
|
||||
return;
|
||||
var outputFile = MusicCacheStorageUtils
|
||||
.getTrackFile(LibVKXClient.asId(track));
|
||||
|
||||
VTExecutors.getMusicDownloadExecutor().submit(() -> new CachedDownloader(
|
||||
new Mp3Downloader(outputFile)
|
||||
).download(track, callback));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
package ru.vtosters.lite.music.interfaces;
|
||||
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Callback {
|
||||
void onProgress(int progress);
|
||||
|
||||
void onSuccess();
|
||||
|
||||
void onFailure();
|
||||
void onFailure(Throwable e);
|
||||
|
||||
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(Throwable e) {
|
||||
completeExceptionally(e);
|
||||
origin.onFailure(e);
|
||||
}
|
||||
|
||||
@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 download(MusicTrack track, Callback callback);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ru.vtosters.lite.utils.music;
|
||||
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import com.vk.dto.music.Artist;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MusicTrackUtils {
|
||||
public static String getArtists(MusicTrack musicTrack) {
|
||||
return (musicTrack.L == null || musicTrack.I == null
|
||||
? normalizeMetadata(musicTrack.C)
|
||||
: normalizeMetadata(musicTrack.L.stream().map(Artist::w1).collect(Collectors.joining(", "))));
|
||||
}
|
||||
|
||||
private static String normalizeMetadata(String in) {
|
||||
return in.replaceAll("[\\\\/:*?\"<>|]", "");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user