diff --git a/app/src/main/aidl/bruhcollective/itaysonlab/libvkx/ILibVkxService.aidl b/app/src/main/aidl/bruhcollective/itaysonlab/libvkx/ILibVkxService.aidl new file mode 100644 index 000000000..7cb0e55ef --- /dev/null +++ b/app/src/main/aidl/bruhcollective/itaysonlab/libvkx/ILibVkxService.aidl @@ -0,0 +1,33 @@ +// IVKExternalMusic.aidl +package bruhcollective.itaysonlab.libvkx; + +interface ILibVkxService { + // Starts playback. playbackContext can be "other" if you don't sure what it is + void play(in List audio_ids, in int index, in String playbackContext) = 1; + + // A UserID. CHECK FOR THIS TO BE EQUAL BEFORE SENDING ANYTHING TO SERVICE! + int getUserId() = 2; + + // Cache add/remove + void addTrackToCache(in int track_id, in int owner_id, in String access_key) = 3; + void deleteTrackFromCache(in int track_id, in int owner_id) = 4; + boolean isTrackCached(in int track_id, in int owner_id) = 5; + + // Get a list of cached tracks. The response is close to VK's original response (an audio list) + // Also, the items in the list are JSON ones. + List getCache() = 6; + + // [ver 1.1] + void downloadTrack(in int track_id, in int owner_id, in String access_key) = 7; + void downloadPlaylist(in int playlist_id, in int owner_id, in String access_key) = 8; + void addPlaylistToCache(in int playlist_id, in int owner_id, in String access_key) = 9; + void deletePlaylistFromCache(in int playlist_id, in int owner_id) = 10; + + // [ver 1.2] + String getCacheCatalog() = 11; + List getTracksInPlaylist(in String playlist_id, in String owner_id) = 12; + String getPlaylistDefJson(in String playlist_id, in String owner_id) = 13; + + // [ver 1.3] + boolean isPlaylistCached(in int playlist_id, in int owner_id) = 14; +} \ No newline at end of file diff --git a/app/src/main/java/bruhcollective/itaysonlab/libvkx/ILibVkxService.java b/app/src/main/java/bruhcollective/itaysonlab/libvkx/ILibVkxService.java deleted file mode 100644 index 0de1327b3..000000000 --- a/app/src/main/java/bruhcollective/itaysonlab/libvkx/ILibVkxService.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * This file is auto-generated. DO NOT MODIFY. - */ -package bruhcollective.itaysonlab.libvkx; - -// Works only with original signature of VT - -public interface ILibVkxService extends android.os.IInterface { - void play(java.util.List audio_ids, int index, String playbackContext) throws android.os.RemoteException; - - int getUserId() throws android.os.RemoteException; - // Starts playback. playbackContext can be "other" if you don't sure what it is - - void addTrackToCache(int track_id, int owner_id, String access_key) throws android.os.RemoteException; - // A UserID. CHECK FOR THIS TO BE EQUAL BEFORE SENDING ANYTHING TO SERVICE! - - void deleteTrackFromCache(int track_id, int owner_id) throws android.os.RemoteException; - // Cache add/remove - - boolean isTrackCached(int track_id, int owner_id) throws android.os.RemoteException; - - java.util.List getCache() throws android.os.RemoteException; - - /** - * Default implementation for ILibVkxService. - */ - class Default implements ILibVkxService { - // Starts playback. playbackContext can be "other" if you don't sure what it is - - @Override - public void play(java.util.List audio_ids, int index, String playbackContext) throws android.os.RemoteException { - } - // A UserID. CHECK FOR THIS TO BE EQUAL BEFORE SENDING ANYTHING TO SERVICE! - - @Override - public int getUserId() throws android.os.RemoteException { - return 0; - } - // Cache add/remove - - @Override - public void addTrackToCache(int track_id, int owner_id, String access_key) throws android.os.RemoteException { - } - - @Override - public void deleteTrackFromCache(int track_id, int owner_id) throws android.os.RemoteException { - } - - @Override - public boolean isTrackCached(int track_id, int owner_id) throws android.os.RemoteException { - return false; - } - // Get a list of cached tracks. The response is close to VK's original response (an audio list) - // Also, the items in the list are JSON ones. - - @Override - public java.util.List getCache() throws android.os.RemoteException { - return null; - } - - @Override - public android.os.IBinder asBinder() { - return null; - } - } - // Get a list of cached tracks. The response is close to VK's original response (an audio list) - // Also, the items in the list are JSON ones. - - /** - * Local-side IPC implementation stub class. - */ - abstract class Stub extends android.os.Binder implements ILibVkxService { - static final int TRANSACTION_play = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); - static final int TRANSACTION_getUserId = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); - static final int TRANSACTION_addTrackToCache = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); - static final int TRANSACTION_deleteTrackFromCache = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); - static final int TRANSACTION_isTrackCached = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5); - static final int TRANSACTION_getCache = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6); - private static final String DESCRIPTOR = "bruhcollective.itaysonlab.libvkx.ILibVkxService"; - - /** - * Construct the stub at attach it to the interface. - */ - public Stub() { - this.attachInterface(this, DESCRIPTOR); - } - - /** - * Cast an IBinder object into an bruhcollective.itaysonlab.libvkx.ILibVkxService interface, - * generating a proxy if needed. - */ - public static ILibVkxService asInterface(android.os.IBinder obj) { - if ((obj == null)) { - return null; - } - android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); - if (((iin != null) && (iin instanceof ILibVkxService))) { - return ((ILibVkxService) iin); - } - return new Proxy(obj); - } - - public static boolean setDefaultImpl(ILibVkxService impl) { - if (Proxy.sDefaultImpl == null && impl != null) { - Proxy.sDefaultImpl = impl; - return true; - } - return false; - } - - public static ILibVkxService getDefaultImpl() { - return Proxy.sDefaultImpl; - } - - @Override - public android.os.IBinder asBinder() { - return this; - } - - @Override - public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { - String descriptor = DESCRIPTOR; - switch (code) { - case INTERFACE_TRANSACTION: { - reply.writeString(descriptor); - return true; - } - case TRANSACTION_play: { - data.enforceInterface(descriptor); - java.util.List _arg0; - _arg0 = data.createStringArrayList(); - int _arg1; - _arg1 = data.readInt(); - String _arg2; - _arg2 = data.readString(); - this.play(_arg0, _arg1, _arg2); - reply.writeNoException(); - return true; - } - case TRANSACTION_getUserId: { - data.enforceInterface(descriptor); - int _result = this.getUserId(); - reply.writeNoException(); - reply.writeInt(_result); - return true; - } - case TRANSACTION_addTrackToCache: { - data.enforceInterface(descriptor); - int _arg0; - _arg0 = data.readInt(); - int _arg1; - _arg1 = data.readInt(); - String _arg2; - _arg2 = data.readString(); - this.addTrackToCache(_arg0, _arg1, _arg2); - reply.writeNoException(); - return true; - } - case TRANSACTION_deleteTrackFromCache: { - data.enforceInterface(descriptor); - int _arg0; - _arg0 = data.readInt(); - int _arg1; - _arg1 = data.readInt(); - this.deleteTrackFromCache(_arg0, _arg1); - reply.writeNoException(); - return true; - } - case TRANSACTION_isTrackCached: { - data.enforceInterface(descriptor); - int _arg0; - _arg0 = data.readInt(); - int _arg1; - _arg1 = data.readInt(); - boolean _result = this.isTrackCached(_arg0, _arg1); - reply.writeNoException(); - reply.writeInt(((_result) ? (1) : (0))); - return true; - } - case TRANSACTION_getCache: { - data.enforceInterface(descriptor); - java.util.List _result = this.getCache(); - reply.writeNoException(); - reply.writeStringList(_result); - return true; - } - default: { - return super.onTransact(code, data, reply, flags); - } - } - } - - private static class Proxy implements ILibVkxService { - public static ILibVkxService sDefaultImpl; - private final android.os.IBinder mRemote; - - Proxy(android.os.IBinder remote) { - mRemote = remote; - } - - @Override - public android.os.IBinder asBinder() { - return mRemote; - } - // Starts playback. playbackContext can be "other" if you don't sure what it is - - public String getInterfaceDescriptor() { - return DESCRIPTOR; - } - // A UserID. CHECK FOR THIS TO BE EQUAL BEFORE SENDING ANYTHING TO SERVICE! - - @Override - public void play(java.util.List audio_ids, int index, String playbackContext) throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - _data.writeStringList(audio_ids); - _data.writeInt(index); - _data.writeString(playbackContext); - boolean _status = mRemote.transact(Stub.TRANSACTION_play, _data, _reply, 0); - if (!_status && getDefaultImpl() != null) { - getDefaultImpl().play(audio_ids, index, playbackContext); - return; - } - _reply.readException(); - } finally { - _reply.recycle(); - _data.recycle(); - } - } - // Cache add/remove - - @Override - public int getUserId() throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - int _result; - try { - _data.writeInterfaceToken(DESCRIPTOR); - boolean _status = mRemote.transact(Stub.TRANSACTION_getUserId, _data, _reply, 0); - if (!_status && getDefaultImpl() != null) { - return getDefaultImpl().getUserId(); - } - _reply.readException(); - _result = _reply.readInt(); - } finally { - _reply.recycle(); - _data.recycle(); - } - return _result; - } - - @Override - public void addTrackToCache(int track_id, int owner_id, String access_key) throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - _data.writeInt(track_id); - _data.writeInt(owner_id); - _data.writeString(access_key); - boolean _status = mRemote.transact(Stub.TRANSACTION_addTrackToCache, _data, _reply, 0); - if (!_status && getDefaultImpl() != null) { - getDefaultImpl().addTrackToCache(track_id, owner_id, access_key); - return; - } - _reply.readException(); - } finally { - _reply.recycle(); - _data.recycle(); - } - } - - @Override - public void deleteTrackFromCache(int track_id, int owner_id) throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - _data.writeInt(track_id); - _data.writeInt(owner_id); - boolean _status = mRemote.transact(Stub.TRANSACTION_deleteTrackFromCache, _data, _reply, 0); - if (!_status && getDefaultImpl() != null) { - getDefaultImpl().deleteTrackFromCache(track_id, owner_id); - return; - } - _reply.readException(); - } finally { - _reply.recycle(); - _data.recycle(); - } - } - // Get a list of cached tracks. The response is close to VK's original response (an audio list) - // Also, the items in the list are JSON ones. - - @Override - public boolean isTrackCached(int track_id, int owner_id) throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - boolean _result; - try { - _data.writeInterfaceToken(DESCRIPTOR); - _data.writeInt(track_id); - _data.writeInt(owner_id); - boolean _status = mRemote.transact(Stub.TRANSACTION_isTrackCached, _data, _reply, 0); - if (!_status && getDefaultImpl() != null) { - return getDefaultImpl().isTrackCached(track_id, owner_id); - } - _reply.readException(); - _result = (0 != _reply.readInt()); - } finally { - _reply.recycle(); - _data.recycle(); - } - return _result; - } - - @Override - public java.util.List getCache() throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - android.os.Parcel _reply = android.os.Parcel.obtain(); - java.util.List _result; - try { - _data.writeInterfaceToken(DESCRIPTOR); - boolean _status = mRemote.transact(Stub.TRANSACTION_getCache, _data, _reply, 0); - if (!_status && getDefaultImpl() != null) { - return getDefaultImpl().getCache(); - } - _reply.readException(); - _result = _reply.createStringArrayList(); - } finally { - _reply.recycle(); - _data.recycle(); - } - return _result; - } - } - } -} diff --git a/app/src/main/java/bruhcollective/itaysonlab/libvkx/client/LibVKXClient.java b/app/src/main/java/bruhcollective/itaysonlab/libvkx/client/LibVKXClient.java index 75cead999..37236da9b 100644 --- a/app/src/main/java/bruhcollective/itaysonlab/libvkx/client/LibVKXClient.java +++ b/app/src/main/java/bruhcollective/itaysonlab/libvkx/client/LibVKXClient.java @@ -81,11 +81,11 @@ public class LibVKXClient { lambdaplay(list, musicTrack, iLibVkxService, null); } - private static String asId(MusicTrack musicTrack) { - String str = musicTrack.e + "_" + musicTrack.d; - if (musicTrack.O == null || musicTrack.O.equals("")) { - return str; + public static String asId(MusicTrack musicTrack) { + String id = musicTrack.e + "_" + musicTrack.d; + if (musicTrack.O != null && !musicTrack.O.equals("")) { + id += "_" + musicTrack.O; } - return str + "_" + musicTrack.O; + return id; } } \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java b/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java index 69f10b80a..cb072c085 100644 --- a/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/downloaders/AudioDownloader.java @@ -1,133 +1,134 @@ package ru.vtosters.lite.downloaders; -import android.os.Environment; -import android.util.Log; +import static ru.vtosters.lite.music.cache.FileCacheImplementation.getTrackFolder; +import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId; +import static ru.vtosters.lite.utils.AndroidUtils.getString; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; +import android.os.Environment; 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.concurrent.Executor; +import java.util.concurrent.Executors; -import ru.vtosters.lite.downloaders.notifications.NotificationChannels; -import ru.vtosters.lite.music.Callback; -import ru.vtosters.lite.music.FFMpeg; -import ru.vtosters.lite.music.M3UDownloader; -import ru.vtosters.lite.music.MP3Downloader; -import ru.vtosters.lite.utils.AndroidUtils; +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; +import ru.vtosters.lite.music.cache.FileCacheImplementation; +import ru.vtosters.lite.music.callback.MusicCallbackBuilder; +import ru.vtosters.lite.music.converter.playlist.PlaylistConverter; +import ru.vtosters.lite.music.downloader.AudioGet; +import ru.vtosters.lite.music.downloader.PlaylistDownloader; +import ru.vtosters.lite.music.downloader.TrackDownloader; +import ru.vtosters.lite.music.notification.MusicNotificationBuilder; +import ru.vtosters.lite.utils.IOUtils; + +/** + * Entrypoint for downloading audio + *

+ * The actual code is in {@link ru.vtosters.lite.music} + */ public class AudioDownloader { - private static NotificationManagerCompat notificationManager = NotificationManagerCompat.from(AndroidUtils.getGlobalContext()); + private static final Executor executor = Executors.newFixedThreadPool(4); - 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); + + var playlistName = IOUtils.getValidFileName(playlist.g); + + var musicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath(); + var downloadPath = musicPath + File.separator + playlistName; + + var notificationId = playlistName.hashCode(); + var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlistName, notificationId); + + PlaylistDownloader.downloadPlaylist( + tracks, + IOUtils.getValidFileName(playlist.g), + downloadPath, + MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId) + ); } - public static void downloadM3U8(MusicTrack track) { - if (track.D == null) { - ToastUtils.a(AndroidUtils.getString("link_audio_error")); + + public static void downloadAudio(MusicTrack track) { + downloadM3U8(track, false); + } + + public static void cacheTrack(MusicTrack track) { + var trackId = track.y1(); + if (CacheDatabaseDelegate.isCached(trackId)) { + CacheDatabaseDelegate.removeTrackFromCache(LibVKXClient.asId(track)); return; } + var trackFile = FileCacheImplementation.getTrackFile(trackId); + if (!trackFile.exists()) + trackFile.getParentFile().mkdirs(); + downloadM3U8(track, true); + MusicNotificationBuilder.notifySavingToCache(track); + CacheDatabaseDelegate.insertTrack(track); + } + + public static void cachePlaylist(Playlist playlist) { + var tracks = PlaylistConverter.getPlaylist(playlist); + + var notificationId = playlist.g.hashCode(); + var notification = MusicNotificationBuilder.buildPlaylistDownloadNotification(playlist.g, notificationId); + + PlaylistDownloader.cachePlaylist( + tracks, + MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId) + ); + } + + public static void cacheAllAudios() { + var tracks = AudioGet.getAudios(); + + var notificationId = getUserId(); + var notification = MusicNotificationBuilder.buildAllAudiosDownloadNotification(notificationId); + + PlaylistDownloader.cachePlaylist( + tracks, + MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId) + ); + } + + public static void downloadAllAudios() { + var tracks = AudioGet.getAudios(); + + var notificationId = getUserId(); + var notification = MusicNotificationBuilder.buildAllAudiosDownloadNotification(notificationId); + + var playlistName = "Audios of " + getUserId(); + 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 downloadPath = musicPath + File.separator + playlistName; + + PlaylistDownloader.downloadPlaylist( + tracks, + playlistName, + downloadPath, + MusicCallbackBuilder.buildPlaylistCallback(tracks.size(), notification, notificationId) + ); + } + + private static void downloadM3U8(MusicTrack track, boolean cache) { + if (track.D == null) { + ToastUtils.a(getString("link_audio_error")); + return; } + var musicPath = cache + ? getTrackFolder(LibVKXClient.asId(track)).getAbsolutePath() + : Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath(); var tempId = track.d; - var downloadPath = musicPath + File.separator + tempId; + var downloadPath = musicPath + File.separator; + var notification = MusicNotificationBuilder.buildDownloadNotification(track, tempId); - var notification = buildDownloadNotification(track, tempId); - - - downloadM3U8(track, downloadPath, new Callback() { - long startTime = 0; - long elapsedTime = 0; - - @Override - public void onProgress(int progress) { - if (elapsedTime > 1000) { - notification.setProgress(100, progress, false); - notificationManager.notify(tempId, notification.build()); - - startTime = System.currentTimeMillis(); - elapsedTime = 0; - } else { - elapsedTime = System.currentTimeMillis() - startTime; - } - } - - @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(); - } - } catch (UnsatisfiedLinkError e) { - Log.e("AudioDownloader", "native libs"); - Log.e("AudioDownloader", e.getMessage()); - onFailure(); - } - } - - @Override - public void onFailure() { - notification.setContentText(AndroidUtils.getString("load_audio_error")).setProgress(0, 0, false); - notificationManager.notify(tempId, notification.build()); - } - - @Override - public void onSizeReceived(long size, long header) { - - } - }); - } - - 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 downloadMP3(MusicTrack track) { - MP3Downloader.execute(track); - } - - private static NotificationCompat.Builder buildDownloadNotification(MusicTrack track, int id) { - var notificationBuilder = new NotificationCompat.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_DOWNLOAD_CHANNEL_ID) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentTitle(AndroidUtils.getString("audio_downloading")) - .setContentText(track.toString()) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setAutoCancel(true); - notificationBuilder.setProgress(100, 0, false); - notificationManager.notify(id, notificationBuilder.build()); - return notificationBuilder; + TrackDownloader.downloadTrack(track, downloadPath, MusicCallbackBuilder.buildOneTrackCallback(tempId, notification)); } } diff --git a/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java b/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java index d8ae04781..e479651f5 100644 --- a/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java +++ b/app/src/main/java/ru/vtosters/lite/downloaders/notifications/NotificationChannels.java @@ -1,5 +1,7 @@ package ru.vtosters.lite.downloaders.notifications; +import static ru.vtosters.lite.utils.AndroidUtils.getString; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -12,12 +14,21 @@ 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 = AndroidUtils.getString("music_dl_title"); - public static final String MUSIC_DOWNLOAD_CHANNEL_DESCRIPTION = AndroidUtils.getString("music_dl_progress"); + public static final String MUSIC_DOWNLOAD_CHANNEL_NAME = getString("music_downloading"); + public static final String MUSIC_DOWNLOAD_CHANNEL_DESCRIPTION = getString("music_downloading_progress"); + + public static final String MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_ID = "music_playlist_download_channel"; + public static final String MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_NAME = getString("playlist_downloading"); + public static final String MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_DESCRIPTION = getString("playlist_downloading_progress"); + + public static final String MUSIC_CACHING_NAME = getString("caching_track_or_pic"); + + private static NotificationManagerCompat notificationManagerCompat; @RequiresApi(api = Build.VERSION_CODES.O) public static void createChannels() { createMusicDownloadChannel(); + createMusicPlaylistDownloadChannel(); } @RequiresApi(api = Build.VERSION_CODES.O) @@ -29,4 +40,20 @@ public class NotificationChannels { var notificationManager = NotificationManagerCompat.from(AndroidUtils.getGlobalContext()); 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()); + return notificationManagerCompat; + } } diff --git a/app/src/main/java/ru/vtosters/lite/hooks/CryptImHook.java b/app/src/main/java/ru/vtosters/lite/hooks/CryptImHook.java index e6a9f0f8b..9830e3e05 100644 --- a/app/src/main/java/ru/vtosters/lite/hooks/CryptImHook.java +++ b/app/src/main/java/ru/vtosters/lite/hooks/CryptImHook.java @@ -7,6 +7,7 @@ import static ru.vtosters.lite.utils.LifecycleUtils.getCurrentActivity; import static ru.vtosters.lite.utils.ThemesUtils.getAlertStyle; import static ru.vtosters.lite.utils.ThemesUtils.getSTextAttr; +import android.app.Activity; import android.content.Context; import android.content.res.ColorStateList; import android.text.Editable; @@ -57,7 +58,14 @@ public class CryptImHook{ } IMProcessor finalEnabled = enabled; - new VkAlertDialog.Builder(getCurrentActivity()) + Activity ctx = getCurrentActivity(); + + if (ctx == null) { + sendToast(getString("app_activities_error")); + return; + } + + new VkAlertDialog.Builder(ctx) .setTitle(getString("encryption_sett")) .setSingleChoiceItems(titles, enabledPosition, (dialog, which) -> { if (which == 0) { @@ -103,6 +111,11 @@ public class CryptImHook{ Context ctx = getCurrentActivity(); + if (ctx == null) { + sendToast(getString("app_activities_error")); + return; + } + LinearLayout linearLayout = new LinearLayout(ctx); final EditText editText = new EditText(ctx); diff --git a/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java b/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java index d58ec2ff5..66cb2b40a 100644 --- a/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java +++ b/app/src/main/java/ru/vtosters/lite/hooks/ui/MusicBottomSheetHook.java @@ -1,6 +1,16 @@ package ru.vtosters.lite.hooks.ui; -import android.util.Log; +import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.*; +import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.asId; +import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.getInstance; +import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.isVkxInstalled; +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.utils.NetworkUtils.isNetworkConnected; +import static ru.vtosters.lite.utils.Preferences.milkshake; + +import android.os.RemoteException; import com.vk.dto.music.MusicTrack; import com.vk.dto.music.Playlist; @@ -9,24 +19,181 @@ import com.vk.music.common.MusicPlaybackLaunchContext; import com.vtosters.lite.R; import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl; +import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.utils.AndroidUtils; public class MusicBottomSheetHook { - public static ArrayList hook(ArrayList actions) { - if (LibVKXClient.isVkxInstalled()) { + private static final ExecutorService executor = Executors.newCachedThreadPool(); + + public static ArrayList hook(ArrayList actions, MusicTrack musicTrack) { + if (musicTrack.F1()) return actions; + + var trackid = asId(musicTrack); + + if (isVkxInstalled()) { actions.add(getPlayInVKXAction()); } + + if (isIntegrationEnabled()) { + if (isVkxCached(trackid)){ + actions.add(getRemoveCacheTrackVkxAction()); + } else { + actions.add(addToCacheTrackVkxAction()); + } + } + + if (isNetworkConnected()){ + actions.add(downloadAsMp3Action()); + } + return actions; } - public static boolean tryPlayInVKX(int actionId, MusicTrack track, MusicPlaybackLaunchContext context, Playlist playlist) { - if (actionId != AndroidUtils.getIdentifier("play_in_vkx", "id")) { - return false; + + public static ArrayList hookDownloadBTN(ArrayList actions, MusicTrack musicTrack) { + if (musicTrack.F1()) return actions; + + var trackid = asId(musicTrack); + + if (isCached(trackid)){ + actions.add(new MusicAction( + AndroidUtils.getIdentifier("music_action_toggle_download", "id"), + milkshake() ? R.drawable.ic_delete_outline_28 : R.drawable.ic_delete_24, + AndroidUtils.getIdentifier("remove_from_cache", "string"), + musicTrack.F1() ? R.color.music_action_button_gray_50_alpha : R.color.caption_gray, + R.string.music_talkback_empty, + false + )); + } else { + actions.add(new MusicAction( + AndroidUtils.getIdentifier("music_action_toggle_download", "id"), + milkshake() ? R.drawable.ic_download_outline_28 : R.drawable.ic_download_24, + AndroidUtils.getIdentifier("add_to_cache", "string"), + musicTrack.F1() ? R.color.music_action_button_gray_50_alpha : R.color.caption_gray, + R.string.music_talkback_empty, + false + )); } + return actions; + } + + public static ArrayList hook(ArrayList actions, Playlist playlist) { + if (playlist.D) return actions; + + if (isVkxInstalled()) actions.add(getPlayInVKXAction()); + + if (isIntegrationEnabled()){ + if (isVkxCached(playlist.a, playlist.b)){ + actions.add(getRemoveCacheTrackVkxAction()); + } else { + actions.add(addToCacheTrackVkxAction()); + } + } else { + actions.add(addToCacheTrackAction()); + } + + if (isNetworkConnected()) actions.add(downloadAsMp3Action()); + + return actions; + } + + public static boolean injectOnClick(int actionId, MusicTrack track, MusicPlaybackLaunchContext context, Playlist playlist) { // musictrack inj + if (actionId == AndroidUtils.getIdentifier("play_in_vkx", "id")) + return tryPlayInVKX(track, context, playlist); + + if (actionId == AndroidUtils.getIdentifier("remove_from_cache_vkx", "id")) { + getInstance().runOnService(service -> { + try { + service.deleteTrackFromCache(track.d, track.e); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + return true; + } else if (actionId == AndroidUtils.getIdentifier("add_to_cache_vkx", "id")) { + getInstance().runOnService(service -> { + try { + service.addTrackToCache(track.d, track.e, (track.J != null) ? track.J : ""); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + return true; + } + + if (actionId == AndroidUtils.getIdentifier("download_mp3", "id")) { + if (isIntegrationEnabled()) { + getInstance().runOnService(service -> { + try { + service.downloadTrack(track.d, track.e, (track.J != null) ? track.J : ""); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + } else { + AudioDownloader.downloadAudio(track); + } + + return true; + } + + return false; + } + + public static boolean injectOnClick(int actionId, Playlist playlist) { // playlist inj + if (actionId == AndroidUtils.getIdentifier("play_in_vkx", "id")) + return tryPlayInVKX(null, null, playlist); + + if (actionId == AndroidUtils.getIdentifier("add_to_cache", "id")) { + executor.submit(() -> AudioDownloader.cachePlaylist(playlist)); + return true; + } + + if (actionId == AndroidUtils.getIdentifier("remove_from_cache_vkx", "id")) { + getInstance().runOnService(service -> { + try { + service.deletePlaylistFromCache(playlist.a, playlist.b); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + return true; + } else if (actionId == AndroidUtils.getIdentifier("add_to_cache_vkx", "id")) { + getInstance().runOnService(service -> { + try { + service.addPlaylistToCache(playlist.a, playlist.b, (playlist.Q != null) ? playlist.Q : ""); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + return true; + } + + if (actionId == AndroidUtils.getIdentifier("download_mp3", "id")) { + if (isIntegrationEnabled()) { + getInstance().runOnService(service -> { + try { + service.downloadPlaylist(playlist.a, playlist.b, (playlist.Q != null) ? playlist.Q : ""); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + } else { + executor.submit(() -> AudioDownloader.downloadPlaylist(playlist)); + } + return true; + } + + return false; + } + + public static boolean tryPlayInVKX(MusicTrack track, MusicPlaybackLaunchContext context, Playlist playlist) { var trackList = new ArrayList(); if (playlist != null) { @@ -35,18 +202,73 @@ public class MusicBottomSheetHook { trackList.add(track); } - LibVKXClientImpl.LibVKXAction action = iLibVkxService -> LibVKXClient.lambdaplay(trackList, track, iLibVkxService, context); - return LibVKXClient.getInstance().runOnService(action); + LibVKXClientImpl.LibVKXAction action = iLibVkxService -> lambdaplay(trackList, track, iLibVkxService, context); + return getInstance().runOnService(action); } private static MusicAction getPlayInVKXAction() { return new MusicAction( AndroidUtils.getIdentifier("play_in_vkx", "id"), - R.drawable.ic_music_24, + milkshake() ? R.drawable.ic_music_outline_24 : R.drawable.ic_music_24, AndroidUtils.getIdentifier("play_in_vkx", "string"), R.color.caption_gray, R.string.music_talkback_empty, false ); } + + private static MusicAction getRemoveCacheTrackAction() { + return new MusicAction( + AndroidUtils.getIdentifier("remove_from_cache", "id"), + milkshake() ? R.drawable.ic_delete_outline_28 : R.drawable.ic_delete_24, + AndroidUtils.getIdentifier("remove_from_cache", "string"), + R.color.caption_gray, + R.string.music_talkback_empty, + false + ); + } + + private static MusicAction addToCacheTrackAction() { + return new MusicAction( + AndroidUtils.getIdentifier("add_to_cache", "id"), + milkshake() ? R.drawable.ic_add_outline_28 : R.drawable.ic_add_24, + AndroidUtils.getIdentifier("add_to_cache", "string"), + R.color.caption_gray, + R.string.music_talkback_empty, + false + ); + } + + private static MusicAction getRemoveCacheTrackVkxAction() { + return new MusicAction( + AndroidUtils.getIdentifier("remove_from_cache_vkx", "id"), + milkshake() ? R.drawable.ic_delete_outline_28 : R.drawable.ic_delete_24, + AndroidUtils.getIdentifier("remove_from_cache_vkx", "string"), + R.color.caption_gray, + R.string.music_talkback_empty, + false + ); + } + + private static MusicAction addToCacheTrackVkxAction() { + return new MusicAction( + AndroidUtils.getIdentifier("add_to_cache_vkx", "id"), + milkshake() ? R.drawable.ic_add_outline_28 : R.drawable.ic_add_24, + AndroidUtils.getIdentifier("add_to_cache_vkx", "string"), + R.color.caption_gray, + R.string.music_talkback_empty, + false + ); + } + + private static MusicAction downloadAsMp3Action() { + return new MusicAction( + AndroidUtils.getIdentifier("download_mp3", "id"), + milkshake() ? R.drawable.ic_download_outline_28 : R.drawable.ic_download_24, + AndroidUtils.getIdentifier("download_mp3", "string"), + R.color.caption_gray, + R.string.music_talkback_empty, + false + ); + } } diff --git a/app/src/main/java/ru/vtosters/lite/music/FFMpeg.java b/app/src/main/java/ru/vtosters/lite/music/FFMpeg.java deleted file mode 100644 index ade05b86b..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/FFMpeg.java +++ /dev/null @@ -1,135 +0,0 @@ -package ru.vtosters.lite.music; - -import android.os.Build; -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.MusicTrack; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.util.Arrays; - -import ru.vtosters.lite.utils.AndroidUtils; -import ru.vtosters.lite.utils.IOUtils; - -public class FFMpeg { - private static final int BUFFER_SIZE = 2048; - - private static void copyAssets() { - try { - String[] files = AndroidUtils.getGlobalContext().getAssets().list("ffmpeg"); - for (String file : files) { - if (file.endsWith(".so")) { - InputStream in = AndroidUtils.getGlobalContext().getAssets().open("ffmpeg" + File.separator + file); - FileOutputStream out = new FileOutputStream(AndroidUtils.getGlobalContext().getFilesDir() + File.separator + file); - byte[] buffer = new byte[BUFFER_SIZE]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - in.close(); - out.close(); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void checkFFMpegLibs() { - var lib = new File(AndroidUtils.getGlobalContext().getFilesDir(), "libffmpegkit_abidetect.so"); - if (!lib.exists()) { - ToastUtils.a(AndroidUtils.getString("ffmpegkit_copy")); - copyAssets(); - } - } - - public static boolean convert(String in, String out, MusicTrack track) { - checkFFMpegLibs(); - - var tsesList = new File(in).listFiles(); - - if (tsesList == null) { - return false; - } - - var mergedTsFile = new File(in, "all.ts"); - var mergedByteArrayStream = new ByteArrayOutputStream(); - - Arrays.sort(tsesList, (o1, o2) -> { - var index1 = Integer.parseInt(o1.getName().split("-")[1]); - var index2 = Integer.parseInt(o2.getName().split("-")[1]); - return index1 - index2; - }); - - for (var file : tsesList) { - if (file.getName().endsWith(".ts")) { - try { - mergedByteArrayStream.write(IOUtils.readAllBytes(file)); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - Files.write(mergedTsFile.toPath(), mergedByteArrayStream.toByteArray()); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } else { - try (FileOutputStream fos = new FileOutputStream(mergedTsFile)) { - fos.write(mergedByteArrayStream.toByteArray()); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - - - // convert .ts file to .mp3 file via ffmpeg - var sb = new StringBuilder(); - sb.append("-y -i ").append(in).append("/all.ts -map 0 -dn "); - sb.append(" -loglevel error"); - sb.append(" -hide_banner"); - if (track.f != null) { - sb.append(" -metadata title='").append(track.f).append("'"); - } - if (track.C != null) { - sb.append(" -metadata artist='").append(track.C).append("'"); - } - if (track.I != null) { - sb.append(" -metadata album='").append(track.I.getTitle()).append("'"); - } -// -// if (track.L != null) { -// var artists = track.L.stream().map(Artist::w1).reduce((s, s2) -> s + ", " + s2).orElse(""); -// sb.append(" -metadata artist='").append(artists).append("'"); -// } - - sb.append(" -c copy '").append(out).append("'"); - - var command = sb.toString(); - - var session = FFmpegKit.execute(command); - var rc = session.getReturnCode(); - - IOUtils.deleteRecursive(new File(in)); - if (ReturnCode.isSuccess(rc)) { - return true; - } - - Log.e("FFMpeg", "FFmpegKit failed with return code " + rc); - return false; - } -} diff --git a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java index 00f4abc6a..f878160e9 100644 --- a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java @@ -1,5 +1,7 @@ package ru.vtosters.lite.music; +import com.vk.dto.music.MusicTrack; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -17,42 +19,71 @@ 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.downloader.ThumbnailDownloader; +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, boolean cache) { var request = new Request.a() - .b(url) + .b(track.D) .a(); client.a(request).a(new okhttp3.Callback() { @Override public void a(Call call, IOException e) { - + callback.onFailure(); } @Override public void a(Call call, Response response) throws IOException { - parse(response.a().g(), outDir, callback); + parse(response.a().g(), outDir, callback, track, cache); } }); } - private static void parse(String payload, File outDir, Callback callback) { + private void parse(String payload, File outDir, Callback callback, MusicTrack track, boolean cache) { + if (cache) + try { + ThumbnailDownloader.downloadThumbnails(track); + } catch (IOException e) { + e.printStackTrace(); + } + VKM3UParser parser = new VKM3UParser(payload); List tses = parser.getTransportStreams(); AtomicInteger progress = new AtomicInteger(0); List> futures = new ArrayList<>(); + var tsesDir = new File(outDir, String.valueOf(track.d)); + tsesDir.mkdirs(); + var resultTs = new File(tsesDir, "result.ts"); + + String title = null; + + if (track.f != null) { + title = track.f; + if (!track.g.isEmpty()) { + title += " (" + track.g + ")"; + } + } + + var fileName = IOUtils.getValidFileName((cache ? "track" : track.C + " - " + title) + ".mp3"); + var resultMp3 = new File(outDir, fileName); + callback.onProgress(5); for (TransportStream ts : tses) { String tsURL = parser.getBaseUrl() + ts.getName(); futures.add(CompletableFuture.runAsync(() -> { try { - // Log.d("M3UDownloader", "Downloading " + tsURL); InputStream is = IOUtils.openStream(tsURL); byte[] content; if (TransportStream.METHOD_AES128.equals(ts.getMethod())) { @@ -61,20 +92,35 @@ 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.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size())); callback.onSizeReceived((long) content.length * tses.size(), parser.getHeapSize()); } catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException e) { - e.printStackTrace(); callback.onFailure(); + throw new RuntimeException(e); } - }).thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size())))); - } - // wait for all futures - for (var future: futures) { - future.join(); + })); } + var future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + future.exceptionally(throwable -> { + callback.onFailure(); + return null; + }) + .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); + } - callback.onSuccess(); + // Initialization-on-demand + private static class Holder { + private static final M3UDownloader INSTANCE = new M3UDownloader(); } } diff --git a/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java b/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java index cf852669a..1d28adf35 100644 --- a/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/MP3Downloader.java @@ -1,87 +1,76 @@ -package ru.vtosters.lite.music; - -import static ru.vtosters.lite.proxy.ProxyUtils.getApi; - -import android.app.DownloadManager; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.util.Log; -import android.widget.Toast; - -import androidx.annotation.RequiresApi; - -import com.vk.dto.music.MusicTrack; - -import org.json.JSONException; -import org.json.JSONObject; - -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.utils.AndroidUtils; - -public class MP3Downloader { - - private static final String URL_FORMAT = "https://" + getApi() + "/method/audio.getById?v=5.131&access_token=%s&audios=%s©right=https://vtosters.app/"; - private static final OkHttpClient client = new OkHttpClient(); - - public static void execute(MusicTrack track) { - var token = AndroidUtils.getPreferences().getString("vk_admin_token", ""); - if (token.isEmpty()) { - Toast.makeText(AndroidUtils.getGlobalContext(), AndroidUtils.getString("update_vk_admin_token"), 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")) { - JSONObject responseNode = new JSONObject(payload).getJSONArray("response") - .getJSONObject(0); - if (responseNode.has("url")) { - var url = responseNode.optString("url"); - Log.d("MP3Downloader", url); - if (url.endsWith(".mp3")) { - downloadMP3(track.toString(), url); - } else if (VKM3UParser.isM3U8(url)) { - AudioDownloader.downloadM3U8(track); - } - } - } - } catch (JSONException e) { - Log.d("MP3Downloader", e + ""); - } - } - }); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private static void downloadMP3(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.DIRECTORY_MUSIC, - title + ".mp3") - .setVisibleInDownloadsUi(true); - downloadManager.enqueue(request); - } -} +//package ru.vtosters.lite.music; +// +//import android.text.TextUtils; +//import android.util.Log; +//import android.widget.Toast; +// +//import com.vk.dto.music.MusicTrack; +// +//import org.json.JSONException; +//import org.json.JSONObject; +// +//import java.io.File; +//import java.io.IOException; +// +//import okhttp3.Call; +//import okhttp3.Callback; +//import okhttp3.OkHttpClient; +//import okhttp3.Request; +//import okhttp3.Response; +//import ru.vtosters.lite.downloaders.AudioDownloader; +//import ru.vtosters.lite.music.interfaces.ITrackDownloader; +//import ru.vtosters.lite.utils.AndroidUtils; +// +//public class MP3Downloader implements ITrackDownloader { +// private static final String URL_FORMAT = "https://api.vk.com/method/audio.getById?v=5.131&access_token=%s&audios=%s©right="; +// +// private static final OkHttpClient client = new OkHttpClient(); +// +// public static void downloadMP3(MusicTrack track) { +// var token = AndroidUtils.getPreferences().getString("vk_admin_token", ""); +// if (token.isEmpty()) { +// Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show(); +// return; +// } +// +// var request = new Request.a() +// .b(String.format(URL_FORMAT, token, track.y1())) +// .a(); +// client.a(request).a(new Callback() { +// @Override +// public void a(Call call, IOException e) { +// Log.d("MP3Downloader", e + ""); +// } +// +// @Override +// public void a(Call call, Response response) throws IOException { +// var payload = response.a().g(); +// Log.d("MP3Downloader", payload); +// try { +// JSONObject jsonObj = new JSONObject(payload); +// if (!jsonObj.has("response")) +// return; +// JSONObject responseNode = new JSONObject(payload).getJSONArray("response") +// .getJSONObject(0); +// if (!responseNode.has("url")) +// return; +// var url = responseNode.optString("url"); +// Log.d("MP3Downloader", url); +// if (TextUtils.isEmpty(url)) +// return; +// if (VKM3UParser.isM3U8(url)) { +// AudioDownloader.downloadM3U8(track); +// return; +// } +// } catch (JSONException e) { +// Log.d("MP3Downloader", e + ""); +// } +// } +// }); +// } +// +// @Override +// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.Callback callback) { +// +// } +//} diff --git a/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java b/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java index 44337e43f..a5d1cc499 100644 --- a/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java +++ b/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java @@ -6,6 +6,7 @@ import static ru.vtosters.lite.utils.AndroidUtils.edit; import static ru.vtosters.lite.utils.AndroidUtils.getDefaultPrefs; import static ru.vtosters.lite.utils.AndroidUtils.getPreferences; import static ru.vtosters.lite.utils.AndroidUtils.sendToast; +import static ru.vtosters.lite.utils.Preferences.autocache; import android.util.Log; @@ -19,6 +20,7 @@ import java.util.ArrayList; import java.util.Map; import java.util.TreeMap; +import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.net.NetCall; import ru.vtosters.lite.net.NetCallback; import ru.vtosters.lite.net.NetClient; @@ -86,6 +88,8 @@ public class Scrobbler { return; } + if (autocache()) AudioDownloader.cacheTrack(musictrack); + scrobbleTrack(duration, artist, title, uid); } diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/CacheDatabaseDelegate.java b/app/src/main/java/ru/vtosters/lite/music/cache/CacheDatabaseDelegate.java new file mode 100644 index 000000000..a3c7dd5f0 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/cache/CacheDatabaseDelegate.java @@ -0,0 +1,344 @@ +package ru.vtosters.lite.music.cache; + +import static ru.vtosters.lite.music.cache.FileCacheImplementation.getCacheDir; +import static ru.vtosters.lite.utils.AndroidUtils.*; +import static ru.vtosters.lite.utils.IOUtils.*; + +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.RemoteException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.vk.core.util.ToastUtils; +import com.vk.dto.music.MusicTrack; + +import org.json.JSONObject; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +import bruhcollective.itaysonlab.libvkx.ILibVkxService; +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl; + +public class CacheDatabaseDelegate { + private static final int DB_VERSION = 0x1; + private static final String DB_NAME = "vt_lite_cache.db"; + private static final String TABLE_NAME = "tracks"; + private static final String COLUMN_ID = "id"; + private static final String COLUMN_TRACK_ID = "trackId"; + private static final String COLUMN_ALBUM_ID = "albumId"; + private static final String COLUMN_TITLE = "title"; + private static final String COLUMN_SUBTITLE = "subtitle"; + private static final String COLUMN_ARTIST = "artist"; + private static final String COLUMN_ALBUM_TITLE = "albumTitle"; + private static final String COLUMN_EXPLICIT = "explicit"; + private static final String COLUMN_HAS_ARTWORK = "hasArtwork"; + private static final String COLUMN_DURATION = "duration"; + private static DbHelper helperInstance; + + protected static DbHelper getHelper() { + if (helperInstance == null) helperInstance = new DbHelper(getGlobalContext()); + return helperInstance; + } + + // You MUST call this method ONLY AFTER TRACK IS DOWNLOADED! + public static void insertTrack(MusicTrack track) { + try { + var json = track.J(); + var albumNode = json.optJSONObject("album"); + + var hasArtwork = false; + var albumId = -1; + var albumName = ""; + + if (albumNode != null) { + hasArtwork = albumNode.has("thumb"); + albumId = albumNode.getInt("id"); + albumName = albumNode.getString("title"); + } + + var formula = String.format( + "INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES ('%s', %s, '%s', '%s', '%s', '%s', %s, %s, %s)", + TABLE_NAME, + COLUMN_TRACK_ID, COLUMN_ALBUM_ID, COLUMN_TITLE, COLUMN_SUBTITLE, COLUMN_ARTIST, COLUMN_ALBUM_TITLE, COLUMN_EXPLICIT, COLUMN_DURATION, COLUMN_HAS_ARTWORK, + track.y1(), + albumId, + URLEncoder.encode(json.getString("title"), "UTF-8"), + URLEncoder.encode(json.optString("subtitle"), "UTF-8"), + URLEncoder.encode(json.getString("artist"), "UTF-8"), + URLEncoder.encode(albumName, "UTF-8"), + json.optInt("is_explicit"), + json.getInt("duration"), + hasArtwork ? 1 : 0 + ); + getHelper().getReadableDatabase().execSQL(formula); + } catch (Exception e) { + e.printStackTrace(); + ToastUtils.a(getString("track_bd_insert_err") + " [" + e.getMessage() + "]", true); + } + } + + public static void insertTracks(List tracks) { + for (var track : tracks) insertTrack(track); + } + + public static void clear() { + getHelper().getReadableDatabase().execSQL(String.format("DELETE FROM %s", TABLE_NAME)); + deleteRecursive(getCacheDir()); + sendToast(getString("cached_tracks_deleted")); + } + + @NonNull + private static JSONObject reconstructFromDb(Cursor cursor) { + try { + var trackId = cursor.getString(getColumnIndex(cursor, COLUMN_TRACK_ID)); + var splits = trackId.split("_"); + var id = Integer.parseInt(splits[1]); + var ownerId = Integer.parseInt(splits[0]); + + return new JSONObject() + .put("url", FileCacheImplementation.getServerPath()) + .put("id", id) + .put("owner_id", ownerId) + .put("title", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_TITLE)), "UTF-8")) + .put("subtitle", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_SUBTITLE)), "UTF-8")) + .put("artist", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_ARTIST)), "UTF-8")) + .put("duration", cursor.getInt(getColumnIndex(cursor, COLUMN_DURATION))) + .put("is_explicit", cursor.getInt(getColumnIndex(cursor, COLUMN_EXPLICIT))) + .put("access_key", 2281488) + .putOpt("album", reconstructAlbumFromDb(cursor, trackId)); + } catch (Exception e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + @Nullable + private static JSONObject reconstructAlbumFromDb(Cursor cursor, String trackId) { + try { + if (!FileCacheImplementation.getTrackThumbnail(trackId, 300).exists() + && !FileCacheImplementation.getTrackThumbnail(trackId, 600).exists() + && !FileCacheImplementation.getTrackThumbnail(trackId, 1200).exists() + ) { + var ownerId = Integer.parseInt(trackId.split("_")[1]); + return new JSONObject() + .put("id", cursor.getInt(getColumnIndex(cursor, COLUMN_ALBUM_ID))) + .put("owner_id", ownerId) + .put("title", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_ALBUM_TITLE)), "UTF-8")) + .putOpt("thumb", reconstructAlbumThumbFromDb(cursor, trackId)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @Nullable + private static JSONObject reconstructAlbumThumbFromDb(Cursor cursor, String trackId) { + try { + if (cursor.getInt(getColumnIndex(cursor, COLUMN_HAS_ARTWORK)) != 0) + new JSONObject() + .put("height", 600) + .put("width", 600) + .putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300))) + .putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600))) + .putOpt("photo_1200", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 1200))); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static int getColumnIndex(Cursor cursor, String name) { + return cursor.getColumnIndexOrThrow(name); + } + + public static boolean hasTracks() { + if (LibVKXClient.isIntegrationEnabled()) { + return true; + } + + var formula = String.format( + "SELECT COUNT(%s) FROM %s", + COLUMN_ID, + TABLE_NAME + ); + var cursor = getHelper().getReadableDatabase().rawQuery(formula, null); + if (cursor != null) { + cursor.moveToFirst(); + int count = cursor.getInt(0); + cursor.close(); + return count > 0; + } + return false; + } + + public static List getTracks() { + return getTracks(String.format( + "SELECT * FROM %s", + TABLE_NAME + )); + } + + public static List getTracksAsPlaylist() { + return getTracks(String.format( + "SELECT * FROM %s WHERE %s > -1 GROUP BY %s HAVING COUNT(*) > 1 ORDER by id DESC", + TABLE_NAME, + COLUMN_ALBUM_ID, + COLUMN_ALBUM_ID + )); + } + + public static List getTracksByAlbum(String id) { + return getTracks(String.format( + "SELECT COUNT(%s) FROM %s WHERE %s = %s", + COLUMN_ID, + TABLE_NAME, + COLUMN_ALBUM_ID, + id + )); + } + + public static List getTracksByAlbumOne(String id) { + return getTracks(String.format( + "SELECT * FROM %s WHERE %s = %s LIMIT 1", + TABLE_NAME, + COLUMN_ALBUM_ID, + id + )); + } + + public static List getTracks(String query) { + List list = new ArrayList<>(); + try (Cursor cursor = getHelper().getReadableDatabase().rawQuery(query, new String[0])) { + while (cursor.moveToNext()) { + list.add(new MusicTrack(reconstructFromDb(cursor))); + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + public static boolean isCached(String trackId) { + var formula = String.format( + "SELECT COUNT(%s) FROM %s WHERE %s = '%s'", + COLUMN_ID, + TABLE_NAME, + COLUMN_TRACK_ID, + trackId + ); + try { + Cursor size = getHelper().getReadableDatabase().rawQuery(formula, null); + if (size != null) { + size.moveToFirst(); + int count = size.getInt(0); + size.close(); + return count != 0; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public static boolean isVkxCached(int playlist_id, int owner_id) { + return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>(){ + @Override + public Boolean run(ILibVkxService service){ + try { + return service.isPlaylistCached(playlist_id, owner_id); + } catch (RemoteException e) { + return false; + } + } + + @Override + public Boolean defaultValue(){ + return false; + } + }); + } + + public static boolean isVkxCached(String trackId) { + return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>(){ + @Override + public Boolean run(ILibVkxService service){ + try { + var splits = trackId.split("_"); + var id = Integer.parseInt(splits[1]); + var owner = Integer.parseInt(splits[0]); + return service.isTrackCached(id, owner); + } catch (RemoteException e) { + return false; + } + } + + @Override + public Boolean defaultValue(){ + return false; + } + }); + } + + public static void removeTrackFromCache(String trackId) { + deleteRecursive(FileCacheImplementation.getTrackFolder(trackId)); + + var formula = String.format( + "DELETE FROM %s WHERE %s = '%s'", + TABLE_NAME, + COLUMN_TRACK_ID, + trackId + ); + getHelper().getWritableDatabase().execSQL(formula); + } + + public static long getTrackCount() { + if (!LibVKXClient.isIntegrationEnabled()) + return DatabaseUtils.queryNumEntries(getHelper().getReadableDatabase(), TABLE_NAME); + return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric() { + @Override + public Long run(ILibVkxService service) { + try { + return (long) service.getCache().size(); + } catch (RemoteException e) { + e.printStackTrace(); + return defaultValue(); + } + } + + @Override + public Long defaultValue() { + return 0L; + } + }); + } + + private static class DbHelper extends SQLiteOpenHelper { + public DbHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + public void onCreate(SQLiteDatabase db) { + var formula = String.format( + "CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s TEXT, %s INTEGER, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s INTEGER, %s INTEGER, %s INTEGER)", + TABLE_NAME, + COLUMN_ID, COLUMN_TRACK_ID, COLUMN_ALBUM_ID, COLUMN_TITLE, COLUMN_SUBTITLE, COLUMN_ARTIST, COLUMN_ALBUM_TITLE, COLUMN_EXPLICIT, COLUMN_DURATION, COLUMN_HAS_ARTWORK + ); + db.execSQL(formula); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/FileCacheImplementation.java b/app/src/main/java/ru/vtosters/lite/music/cache/FileCacheImplementation.java new file mode 100644 index 000000000..68c7f35ca --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/cache/FileCacheImplementation.java @@ -0,0 +1,60 @@ +package ru.vtosters.lite.music.cache; + +import android.content.Context; +import android.net.Uri; + +import com.vk.dto.music.MusicTrack; + +import java.io.File; + +import ru.vtosters.lite.utils.AndroidUtils; +import ru.vtosters.lite.utils.IOUtils; + +public class FileCacheImplementation { + + public static void clear() { + CacheDatabaseDelegate.clear(); + IOUtils.deleteRecursive(getCacheDir(), false); + } + + public static File getCacheDir() { + return AndroidUtils.getGlobalContext().getDir("vt_tracks", Context.MODE_PRIVATE); + } + + public static File getTrackFolder(String trackId) { + var dir = new File(getCacheDir(), trackId); + dir.mkdirs(); + return dir; + } + + public static File getThumbnailsFolder(String trackId) { + var dir = new File(getTrackFolder(trackId), "thumbs"); + dir.mkdirs(); + return dir; + } + + public static File getTrackFile(String trackId) { + return new File(getTrackFolder(trackId), "track.mp3"); + } + + public static boolean isTrackExist(String trackId) { + return CacheDatabaseDelegate.isCached(trackId); + } + + public static File getTrackFile(MusicTrack track) { + return getTrackFile(track.y1()); + } + + public static File getTrackThumbnail(String trackId, int factor) { + return new File(getThumbnailsFolder(trackId), "thumb_" + factor + ".png"); + } + + public static String getFileUri(File file) { + if (!file.exists()) return null; + return Uri.fromFile(file).toString(); + } + + public static String getServerPath() { + return "https://vtosters.app"; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/helpers/PlaylistHelper.java b/app/src/main/java/ru/vtosters/lite/music/cache/helpers/PlaylistHelper.java new file mode 100644 index 000000000..9d977acd8 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/cache/helpers/PlaylistHelper.java @@ -0,0 +1,93 @@ +package ru.vtosters.lite.music.cache.helpers; + +import static ru.vtosters.lite.utils.AndroidUtils.getString; + +import com.vk.dto.music.Playlist; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; +import ru.vtosters.lite.music.cache.FileCacheImplementation; +import ru.vtosters.lite.utils.AccountManagerUtils; + +public class PlaylistHelper { + + public static Playlist createCachedPlaylistMetadata() { + try { + var obj = new JSONObject() + .put("id", -1) + .put("owner_id", AccountManagerUtils.getUserId()) + .put("type", 0) + .put("album_type", "playlist") + .put("title", getString("cached_tracks_title")) + .put("description", getString("cached_tracks_desc")) + .put("count", 0); + return new Playlist(obj); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + public static Playlist createCachedPlaylistMetadata(String id) { + var track = TracklistHelper.getTracksOne(id).get(0); + var albumLink = track.I; + var thumb = albumLink.u1(); + return createAlbum( + thumb != null ? thumb.h(600) : "", + albumLink.getTitle(), + track.C, + track.y1(), + albumLink.getId() + ); + } + + public static Playlist createAlbum(String photo, String title, String artist, String trackId, int albumId) { + try { + var jPhoto = new JSONObject(); + if (!photo.isEmpty()) + jPhoto.put("height", 600) + .put("width", 600) + .putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300))) + .putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600))); + var obj = new JSONObject() + .put("id", -2) + .put("owner_id", albumId) + .put("type", 1) + .put("album_type", "main_only") + .put("title", title) + .put("description", albumId + "/ " + getString("cached_album_warning")) + .put("main_artists", new JSONArray() + .put(new JSONObject().put("name", artist) + .put("id", "-1") + .put("domain", "-1"))) + .putOpt("photo", jPhoto) + .put("count", 0); + return new Playlist(obj); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + public static List getAlbumPlaylists() { + List list = new ArrayList<>(); + for (var track : CacheDatabaseDelegate.getTracksAsPlaylist()) { + var albumLink = track.I; + var thumb = albumLink.u1(); + list.add(createAlbum( + thumb != null ? thumb.h(600) : "", + albumLink.getTitle(), + track.C, + track.y1(), + albumLink.getId() + )); + } + return list; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/helpers/TracklistHelper.java b/app/src/main/java/ru/vtosters/lite/music/cache/helpers/TracklistHelper.java new file mode 100644 index 000000000..f80134495 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/cache/helpers/TracklistHelper.java @@ -0,0 +1,74 @@ +package ru.vtosters.lite.music.cache.helpers; + +import android.util.Log; + +import com.vk.dto.music.MusicTrack; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.net.MalformedURLException; +import java.util.List; + +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; +import ru.vtosters.lite.music.cache.FileCacheImplementation; + +public class TracklistHelper { + + public static List getTracks() { + return CacheDatabaseDelegate.getTracks(); + } + + public static List getTracks(String id) { + return CacheDatabaseDelegate.getTracksByAlbum(id); + } + + public static List getTracksOne(String id) { + return CacheDatabaseDelegate.getTracksByAlbumOne(id); + } + + public static JSONArray tracksToIds(List tracks) { + var arr = new JSONArray(); + for (MusicTrack track : tracks) { + arr.put(track.y1()); + } + return arr; + } + + public static JSONArray tracksToJsons(List tracks) { + var arr = new JSONArray(); + for (MusicTrack track : tracks) { + var json = track.J(); + var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track)); + try { + addCachedThumbnails(json, folder); + } catch (JSONException | MalformedURLException e) { + Log.d("TracklistHelper", e+""); + } + arr.put(json); + } + return arr; + } + + private static void addCachedThumbnails(JSONObject target, File thumbnailsDir) + throws JSONException, MalformedURLException { + var sizes = new JSONArray(); + var files = thumbnailsDir.listFiles(); + if (files == null || files.length == 0) + return; + for (var file : files) { + var filename = file.getName(); + var width = Integer.parseInt(filename.substring(6, filename.length() - 4)); + var src = file.toURI().toURL().toString(); + Log.d("tracksToJsons", src); + var size = new JSONObject() + .put("width", width) + .put("src", src); + sizes.put(size); + } + target.put("album", new JSONObject().put("thumb", new JSONObject().put("sizes", sizes))); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java b/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java new file mode 100644 index 000000000..dadeab312 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/cache/injectors/PlaylistInjector.java @@ -0,0 +1,106 @@ +package ru.vtosters.lite.music.cache.injectors; + +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import com.vk.api.audio.AudioGetPlaylist; +import com.vk.dto.music.MusicTrack; +import com.vk.dto.music.Playlist; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import io.reactivex.Observable; +import io.reactivex.ObservableOnSubscribe; +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; +import ru.vtosters.lite.utils.NetworkUtils; + +public class PlaylistInjector { + + public final static String CHANNEL_NAME = "VTCH"; + + public static void injectDownloadPlaylist(Playlist playlist) { + AudioDownloader.downloadPlaylist(playlist); + } + + public static boolean eligibleForOfflineCaching() { + Log.d("PlaylistInjector", "eligibleForOfflineCaching"); + + return !NetworkUtils.isNetworkConnected() && CacheDatabaseDelegate.hasTracks(); + } + + public static Observable injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) { + Log.d("PlaylistInjector", "injectGetPlaylist"); + + try { + if (!eligibleForOfflineCaching()) + return null; + var requestArgs = audioGetPlaylist.b(); + + var id = requestArgs.get("id"); + var ownerId = requestArgs.get("owner_id"); + var accessKey = requestArgs.get("access_key"); + boolean isVirtualPlaylist = accessKey != null && (accessKey.equals("cache")); + boolean isAlbumVirtualPlaylist = accessKey != null && (accessKey.equals("cacheAlbum")); + + Log.d("PlaylistInjector", "id = " + id + " / owner = " + ownerId + " / access = " + accessKey + " / " + isVirtualPlaylist + " + " + isAlbumVirtualPlaylist); + + if (TextUtils.isEmpty(id) || (!isVirtualPlaylist && !isAlbumVirtualPlaylist)) + return null; + + return Observable.a((ObservableOnSubscribe) observableEmitter -> { + AudioGetPlaylist.c response = new AudioGetPlaylist.c(); + + if (LibVKXClient.isIntegrationEnabled()) { + LibVKXClient.getInstance().runOnService((service) -> { + try { + List cache; + ArrayList tracks = new ArrayList<>(); + + if (isAlbumVirtualPlaylist) { + response.b = new Playlist(new JSONObject(service.getPlaylistDefJson(id, ownerId))); + cache = service.getTracksInPlaylist(id, ownerId); + } else { + response.b = PlaylistHelper.createCachedPlaylistMetadata(); + cache = service.getCache(); + } + + for (String json : cache) { + tracks.add(new MusicTrack(new JSONObject(json))); + } + + response.c = tracks; + } catch (RemoteException | JSONException e) { + e.printStackTrace(); + response.c = new ArrayList<>(); + } + observableEmitter.b(response); + }); + + return; + } + + if (isVirtualPlaylist) { + response.c = (ArrayList) TracklistHelper.getTracks(); + response.b = PlaylistHelper.createCachedPlaylistMetadata(); + } else { + response.c = (ArrayList) TracklistHelper.getTracks(requestArgs.get("owner_id")); + response.b = PlaylistHelper.createCachedPlaylistMetadata(requestArgs.get("owner_id")); + } + + observableEmitter.b(response); + }); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/cache/injectors/TracklistInjector.java b/app/src/main/java/ru/vtosters/lite/music/cache/injectors/TracklistInjector.java new file mode 100644 index 000000000..d4254bdd7 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/cache/injectors/TracklistInjector.java @@ -0,0 +1,223 @@ +package ru.vtosters.lite.music.cache.injectors; + +import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.hasTracks; +import static ru.vtosters.lite.utils.AndroidUtils.getString; + +import android.util.Log; + +import com.vk.catalog2.core.CatalogParser; +import com.vk.catalog2.core.api.dto.CatalogResponse; +import com.vk.dto.music.MusicTrack; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import io.reactivex.Observable; +import io.reactivex.ObservableOnSubscribe; +import ru.vtosters.lite.music.cache.helpers.PlaylistHelper; +import ru.vtosters.lite.music.cache.helpers.TracklistHelper; +import ru.vtosters.lite.utils.AccountManagerUtils; +import ru.vtosters.lite.utils.NetworkUtils; + +public class TracklistInjector { + + public static boolean eligibleForOfflineCaching() { + return !NetworkUtils.isNetworkConnected(); + } + + public static Observable createOfflineRx(CatalogParser parser) { + Log.d("TracklistInjector", "createOfflineRx"); + + return Observable.a((ObservableOnSubscribe) observableEmitter -> { + if (LibVKXClient.isIntegrationEnabled()) { + LibVKXClient.getInstance().runOnService((service) -> { + try { + try { + Log.d("TracklistInjector", "Using vkx cache catalog"); + observableEmitter.b(parser.c(new JSONObject(service.getCacheCatalog()))); + } catch (Exception e){ + e.printStackTrace(); + List tracks = new ArrayList<>(); + List cache = service.getCache(); + + for (String json : cache) { + Log.d("TracklistInjector", "added " + json); + tracks.add(new MusicTrack(new JSONObject(json))); + } + + Log.d("TracklistInjector", "Using own cache catalog from vkx"); + observableEmitter.b(parser.c(createVirtualCatalog(tracks))); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } else { + if (!hasTracks()){ + observableEmitter.b(parser.c(getEmptyCatalog())); + } else { + observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks()))); + } + } + }); + } + + private static JSONObject getEmptyCatalog() throws JSONException{ + return new JSONObject("{\"catalog\":{\"default_section\":\"cache\",\"sections\":[{\"id\":\"cache\",\"title\":\"" + getString("cached_content_title") + "\",\"blocks\":[{\"data_type\":\"placeholder\",\"id\":\"0\",\"layout\":{\"name\":\"placeholder_big\"},\"placeholder_ids\":[\"_synth_trackEmpty\"]}]}]},\"audios\":[],\"playlists\":[],\"placeholders\":[{\"id\":\"_synth_trackEmpty\",\"title\":\"" + getString("cached_content_empty") + "\",\"text\":\"" + getString("cached_content_empty_desc") + "\",\"buttons\":[]}]}"); + } + + public static void injectIntoExistingCatalog(JSONObject catalogNode) { + Log.d("TracklistInjector", "injectIntoExistingCatalog"); + + if (!hasTracks()) return; + try { + var sectionsNode = catalogNode.getJSONObject("catalog") + .getJSONArray("sections"); + var firstSection = sectionsNode.getJSONObject(0); + if (!firstSection.getString("url").equals("https://vk.com/audios" + AccountManagerUtils.getUserId() + "?section=all")) + return; + + var foundPlaylistBlock = false; + var suitableIndex = 0; + + var blocks = firstSection.getJSONArray("blocks"); + for (int i = 0; i < blocks.length(); i++) { + var block = blocks.getJSONObject(i); + var dataType = block.getString("data_type"); + if ("music_audios".equals(dataType)) { + suitableIndex = i - 1; + continue; + } + + if (!"music_playlists".equals(dataType)) + continue; + + var playlistIds = block.getJSONArray("playlists_ids"); + var newPlaylistIds = new JSONArray() + .put(AccountManagerUtils.getUserId() + "_-1"); + + for (int j = 0; j < playlistIds.length(); j++) + newPlaylistIds.put(playlistIds.get(j)); + + block.put("playlists_ids", newPlaylistIds); + + // need test + blocks.remove(i); + blocks.put(i, block); + + foundPlaylistBlock = true; + } + + if (foundPlaylistBlock) { + var playlists = catalogNode.getJSONArray("playlists") + .put(0, PlaylistHelper.createCachedPlaylistMetadata().J()); + catalogNode.put("playlists", playlists); + return; + } + + var ids = new JSONArray() + .put(0, AccountManagerUtils.getUserId() + "_-1"); + + var playlistsBlock = new JSONObject() + .put("id", getRandomId()) + .put("data_type", "music_playlists") + .put("url", "synth:cache:playlists") + .put("playlists_ids", ids); + + var layout = new JSONObject() + .put("name", "large_slider") + .put("owner_id", AccountManagerUtils.getUserId()); + + playlistsBlock.put("layout", layout); + + blocks.put(suitableIndex, playlistsBlock); + + firstSection.put("blocks", blocks); + + var playlists = new JSONArray() + .put(0, PlaylistHelper.createCachedPlaylistMetadata().J()); + catalogNode.put("playlists", playlists); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + public static JSONObject createVirtualCatalog(List tracks) + throws JSONException { + Log.d("TracklistInjector", "createVirtualCatalog"); + + var clickAction = new JSONObject() + .put("type", "open_url") + .put("target", "internal") + .put("url", "https://vkx.app/index.html?app=vt_ctg"); + + var catalogBanner = new JSONObject() + .put("id", 1234) + .put("image_mode", "cover") + .put("title", getString("vkx_integration_title")) + .put("text", getString("vkx_integration_desc")) + .put("click_action", new JSONObject().put("action", clickAction)); + var catalogBanners = new JSONArray() + .put(catalogBanner); + + var audiosBlock = new JSONObject() + .put("id", getRandomId()) + .put("data_type", "music_audios") + .put("url", "synth:cache:list") + .put("audios_ids", TracklistHelper.tracksToIds(tracks)); + + var layout = new JSONObject() + .put("name", "list") + .put("owner_id", AccountManagerUtils.getUserId()); + + audiosBlock.put("layout", layout); + + var blocks = new JSONArray() + .put(audiosBlock); + +// var vkxPostirony = new JSONObject() +// .put("id", getRandomId()) +// .put("data_type", "catalog_banners") +// .put("url", "synth:cache:vkx") +// .put("catalog_banner_ids", new JSONArray().put(1234)); +// +// var vkxlayout = new JSONObject() +// .put("name", "large_list") +// .put("owner_id", Globals.getUserId()) +// .put("infinite_repeat", true); +// +// vkxPostirony.put("layout", vkxlayout); +// +// if (eligibleForVkxAd()) +// blocks.put(vkxPostirony); + var randomId = getRandomId(); + + var section = new JSONObject() + .put("id", randomId) + .put("title", getString("saved_tracks_title")) + .put("url", "synth:cache") + .put("blocks", blocks); + + var sections = new JSONArray() + .put(section); + + var catalog = new JSONObject() + .put("sections", sections) + .put("default_section", randomId); + + return new JSONObject() + .put("catalog", catalog) + .put("audios", TracklistHelper.tracksToJsons(tracks)) + .put("catalog_banners", catalogBanners); + } + + private static String getRandomId() { + return UUID.randomUUID().toString(); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java b/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java new file mode 100644 index 000000000..abff8eabb --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/callback/MusicCallbackBuilder.java @@ -0,0 +1,94 @@ +package ru.vtosters.lite.music.callback; + +import static ru.vtosters.lite.utils.AndroidUtils.getString; + +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import ru.vtosters.lite.downloaders.notifications.NotificationChannels; +import ru.vtosters.lite.music.Callback; + +public class MusicCallbackBuilder { + private static NotificationManagerCompat notificationManager = NotificationChannels.getNotificationManager(); + + public static Callback buildOneTrackCallback(int notificationId, NotificationCompat.Builder notification) { + return new Callback() { + long startTime = 0; + long elapsedTime = 0; + + @Override + public void onProgress(int progress) { + if (elapsedTime > 1000) { + notification.setProgress(100, progress, false); + notificationManager.notify(notificationId, notification.build()); + + startTime = System.currentTimeMillis(); + elapsedTime = 0; + } else { + elapsedTime = System.currentTimeMillis() - startTime; + } + } + + @Override + public void onSuccess() { + try { + notification + .setContentText(getString("player_download_finished")) + .setProgress(0, 0, false) + .setSmallIcon(android.R.drawable.stat_sys_download_done); + notificationManager.notify(notificationId, notification.build()); + } catch (UnsatisfiedLinkError e) { + Log.e("AudioDownloader", "native libs"); + Log.e("AudioDownloader", e.getMessage()); + onFailure(); + } + } + + @Override + public void onFailure() { + notification.setContentText(getString("load_audio_error")).setProgress(0, 0, false); + notificationManager.notify(notificationId, notification.build()); + } + + @Override + public void onSizeReceived(long size, long header) { + + } + }; + } + + public static Callback buildPlaylistCallback(int playlistSize, NotificationCompat.Builder notification, int notificationId) { + return new Callback() { + @Override + public void onProgress(int progress) { + if (progress == playlistSize) { + onSuccess(); + } else { + notification.setContentText(String.format(getString("playlist_download_notf"), progress, playlistSize)); + notification.setProgress(playlistSize, progress, false); + } + notificationManager.notify(notificationId, notification.build()); + } + + @Override + public void onSuccess() { + notification.setContentText(getString("player_download_finished")); + notification.setProgress(0, 0, false); + } + + @Override + public void onFailure() { + notification.setContentText(getString("load_audio_error")).setProgress(0, 0, false); + notificationManager.notify(notificationId, notification.build()); + } + + @Override + public void onSizeReceived(long size, long header) { + + } + }; + } + +} diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java b/app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java new file mode 100644 index 000000000..dbf01e905 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/converter/playlist/PlaylistConverter.java @@ -0,0 +1,86 @@ +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 java.util.stream.Collectors; + +import java8.util.concurrent.CompletableFuture; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import ru.vtosters.lite.utils.AccountManagerUtils; +import ru.vtosters.lite.utils.AndroidUtils; + +/** + * stuff for converting playlists DTO + */ +public class PlaylistConverter { + public static List getPlaylist(Playlist oldPlaylist) { + var tracks = requestPlaylist(0, oldPlaylist.b, oldPlaylist.Q, oldPlaylist.a); + return tracks.stream().filter(track -> !track.D.isEmpty()).collect(Collectors.toList()); + } + + private static List requestPlaylist(int offset, int ownerId, String accessKey, int id) { + var audioCount = 1000; + var requestUrl = "https://" + getApi() + "/method/execute.getPlaylist" + + "?v=5.119" + + "&https=1" + + "&audio_count=" + audioCount + + "&need_playlist=1" + + "&device_id=" + DeviceIdProvider.d(AndroidUtils.getGlobalContext()) + + "&owner_id=" + ownerId + + "&access_key=" + accessKey + + "&func_v=5" + + "&id=" + id + + "&lang=" + getLocale() + + "&audio_offset=" + offset + + "&need_owner=1" + + "&access_token=" + AccountManagerUtils.getUserToken(); + + var request = new Request.a() + .b(requestUrl) + .a(Headers.a("User-Agent", Network.l.c().a(), "Content-Type", "application/x-www-form-urlencoded; charset=utf-8")).a(); + + try { + var response = CompletableFuture.supplyAsync(() -> { + try { + return new OkHttpClient().a(request).execute().a().g(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ).get(); + var jsonObj = new JSONObject(response); + if (!jsonObj.has("response")) + return null; + var playlist = new JSONObject(response).getJSONObject("response").getJSONArray("audios"); + var tracks = new ArrayList(); + for (int i = 0; i < playlist.length(); i++) { + var track = new MusicTrack(playlist.getJSONObject(i)); + tracks.add(track); + } + + if (tracks.size() == audioCount) { + tracks.addAll(requestPlaylist(offset + audioCount, ownerId, accessKey, id)); + } + + return tracks; + } catch (JSONException | ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java new file mode 100644 index 000000000..5a735d0cc --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/FFMpeg.java @@ -0,0 +1,105 @@ +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"); + + String title = null; + + if (track.f != null) { + title = track.f; + if (!track.g.isEmpty()) { + title += " (" + track.g + ")"; + } + } + + if (title != null) { + sb.append(" -metadata title=\"").append(IOUtils.getValidFileName(title)).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 { + String[] files = AndroidUtils.getGlobalContext().getAssets().list("ffmpeg"); + for (String file : files) { + if (file.endsWith(".so")) { + InputStream in = AndroidUtils.getGlobalContext().getAssets().open("ffmpeg" + File.separator + file); + FileOutputStream out = new FileOutputStream(AndroidUtils.getGlobalContext().getFilesDir() + File.separator + file); + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + in.close(); + out.close(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void checkFFMpegLibs() { + var lib = new File(AndroidUtils.getGlobalContext().getFilesDir(), "libffmpegkit_abidetect.so"); + if (!lib.exists()) { + ToastUtils.a(AndroidUtils.getString("ffmpegkit_copy")); + copyAssets(); + } + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java new file mode 100644 index 000000000..958441110 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java @@ -0,0 +1,64 @@ +package ru.vtosters.lite.music.converter.ts; + +import android.os.Build; + +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 ru.vtosters.lite.utils.IOUtils; + +public class TSMerger { + /** + * Merge multiple .ts files into one .ts file + * @param in Directory with .ts files + * @return True if success, else false + */ + public static boolean merge(File in, File out) { + var tsesList = in.listFiles(); + + if (tsesList == null) { + return false; + } + + var mergedByteArrayStream = new ByteArrayOutputStream(); + + Arrays.sort(tsesList, (o1, o2) -> { + var index1 = Integer.parseInt(o1.getName().split("-")[1]); + var index2 = Integer.parseInt(o2.getName().split("-")[1]); + return index1 - index2; + }); + + for (var file : tsesList) { + if (file.getName().endsWith(".ts")) { + try { + mergedByteArrayStream.write(IOUtils.readAllBytes(file)); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + Files.write(out.toPath(), mergedByteArrayStream.toByteArray()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } else { + try (FileOutputStream fos = new FileOutputStream(out)) { + fos.write(mergedByteArrayStream.toByteArray()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + return true; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/AudioGet.java b/app/src/main/java/ru/vtosters/lite/music/downloader/AudioGet.java new file mode 100644 index 000000000..e5c2069ae --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/AudioGet.java @@ -0,0 +1,81 @@ +package ru.vtosters.lite.music.downloader; +import static ru.vtosters.lite.hooks.DateHook.getLocale; +import static ru.vtosters.lite.proxy.ProxyUtils.getApi; +import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId; +import static ru.vtosters.lite.utils.AccountManagerUtils.getUserToken; + +import android.util.Log; + +import com.vk.core.network.Network; +import com.vk.core.util.DeviceIdProvider; +import com.vk.dto.music.MusicTrack; + +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 java.util.stream.Collectors; + +import java8.util.concurrent.CompletableFuture; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import ru.vtosters.lite.utils.AndroidUtils; + +public class AudioGet{ + public static List getAudios(){ + var tracks = requestAudios(0); + return tracks != null ? tracks.stream().filter(track -> !track.D.isEmpty()).collect(Collectors.toList()) : null; + } + + private static List requestAudios(int offset){ + var audioCount = 1000; + var requestUrl = "https://" + getApi() + "/method/audio.get" + + "?v=5.119" + + "&https=1" + + "&count=" + audioCount + + "&device_id=" + DeviceIdProvider.d(AndroidUtils.getGlobalContext()) + + "&owner_id=" + getUserId() + + "&access_key=" + getUserToken() + + "&lang=" + getLocale() + + "&offset=" + offset + + "&access_token=" + 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; + Log.d("AudioGet", jsonObj.toString()); + var audios = new JSONObject(response).getJSONObject("response").getJSONArray("items"); + var tracks = new ArrayList(); + for (int i = 0; i < audios.length(); i++) { + var track = new MusicTrack(audios.getJSONObject(i)); + tracks.add(track); + } + + if (tracks.size() == audioCount) { + tracks.addAll(requestAudios(offset + audioCount)); + } + + return tracks; + } catch (JSONException | ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java new file mode 100644 index 000000000..7467cef1f --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/PlaylistDownloader.java @@ -0,0 +1,77 @@ +package ru.vtosters.lite.music.downloader; + +import android.util.Log; + +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 ru.vtosters.lite.downloaders.notifications.NotificationChannels; +import ru.vtosters.lite.music.Callback; +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; + +public class PlaylistDownloader { + private static NotificationManagerCompat notificationManager = NotificationChannels.getNotificationManager(); + + public static void downloadPlaylist(List playlist, String playlistName, String path, Callback callback) { + File 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(0); + + for (MusicTrack musicTrack : playlist) { + TrackDownloader.downloadTrack(musicTrack, path, new Callback() { + @Override + public void onProgress(int progress) {} + + @Override + public void onSuccess() { + var currentProgress = downloadedTracks.incrementAndGet(); + callback.onProgress(currentProgress); + } + + @Override + public void onFailure() { + callback.onFailure(); + } + + @Override + public void onSizeReceived(long size, long header) {} + }); + } + } + + public static void cachePlaylist(List playlist, Callback callback) { + var downloadedTracks = new AtomicInteger(0); + + for (MusicTrack musicTrack : playlist) { + TrackDownloader.cacheTrack(musicTrack, new Callback() { + @Override + public void onProgress(int progress) {} + + @Override + public void onSuccess() { + var currentProgress = downloadedTracks.incrementAndGet(); + callback.onProgress(currentProgress); + CacheDatabaseDelegate.insertTrack(musicTrack); + } + + @Override + public void onFailure() { + callback.onFailure(); + } + + @Override + public void onSizeReceived(long size, long header) {} + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java new file mode 100644 index 000000000..4b422c288 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java @@ -0,0 +1,49 @@ +package ru.vtosters.lite.music.downloader; + +import android.util.Log; + +import com.vk.dto.music.MusicTrack; + +import java.io.IOException; +import java.net.URL; + +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import ru.vtosters.lite.music.cache.FileCacheImplementation; +import ru.vtosters.lite.utils.IOUtils; + +public class ThumbnailDownloader { + + public static void downloadThumbnails(MusicTrack track) + throws IOException { + var obj = track.J(); + var trackId = LibVKXClient.asId(track); + + if (!obj.has("album")) return; + var album = obj.optJSONObject("album"); + + if (!album.has("thumb")) return; + var thumb = album.optJSONObject("thumb"); + + if (thumb.has("sizes")) { + var sizes = thumb.optJSONArray("sizes"); + for (int i = 0; i < sizes.length(); i++) { + var size = sizes.optJSONObject(i); + var width = size.optInt("width"); + var src = size.optString("src"); + downloadThumbnail(src, width, trackId); + } + } else { + var names = thumb.names(); + for (int i = 0; i < names.length(); i++) { + var name = names.optString(i); + downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId); + } + } + } + + private static void downloadThumbnail(String url, int res, String trackId) + throws IOException { + var data = IOUtils.readAllBytes(new URL(url).openStream()); + IOUtils.writeToFile(FileCacheImplementation.getTrackThumbnail(trackId, res), data); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java new file mode 100644 index 000000000..9f38a2f9b --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/TrackDownloader.java @@ -0,0 +1,34 @@ +package ru.vtosters.lite.music.downloader; + +import android.util.Log; + +import com.vk.dto.music.MusicTrack; + +import java.io.File; + +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import ru.vtosters.lite.music.Callback; +import ru.vtosters.lite.music.M3UDownloader; +import ru.vtosters.lite.music.cache.FileCacheImplementation; +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(), false); + } + + public static void cacheTrack(MusicTrack track, Callback callback) { + var path = FileCacheImplementation.getTrackFolder(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.mkdir()) + Log.v("TrackDownloader", "Directory created"); + else + Log.e("TrackDownloader", "Directory creation failed"); + downloader.downloadTrack(track, outDir, callback, cache); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java b/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java new file mode 100644 index 000000000..52b30dc2b --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/interfaces/ITrackDownloader.java @@ -0,0 +1,20 @@ +package ru.vtosters.lite.music.interfaces; + +import com.vk.dto.music.MusicTrack; + +import java.io.File; + +import ru.vtosters.lite.music.Callback; + +/** + * Interface for track downloaders + */ +public interface ITrackDownloader { + /** + * Download track + * @param track Music track + * @param outDir Directory where to download the track + * @param callback Callback to call when download status changes + */ + void downloadTrack(MusicTrack track, File outDir, Callback callback, boolean cache); +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/music/notification/MusicNotificationBuilder.java b/app/src/main/java/ru/vtosters/lite/music/notification/MusicNotificationBuilder.java new file mode 100644 index 000000000..e701ab1a6 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/notification/MusicNotificationBuilder.java @@ -0,0 +1,73 @@ +package ru.vtosters.lite.music.notification; + +import static ru.vtosters.lite.utils.AndroidUtils.getString; + +import android.app.Notification; +import android.os.Build; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.vk.dto.music.MusicTrack; + +import ru.vtosters.lite.downloaders.notifications.NotificationChannels; +import ru.vtosters.lite.utils.AccountManagerUtils; +import ru.vtosters.lite.utils.AndroidUtils; + +public class MusicNotificationBuilder { + private static NotificationManagerCompat notificationManager = NotificationChannels.getNotificationManager(); + + public static NotificationCompat.Builder buildDownloadNotification(MusicTrack track, int id) { + var notificationBuilder = new NotificationCompat.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_DOWNLOAD_CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(getString("audio_downloading")) + .setContentText(track.toString()) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setAutoCancel(true) + .setProgress(100, 0, false); + notificationManager.notify(id, notificationBuilder.build()); + return notificationBuilder; + } + + public static NotificationCompat.Builder buildPlaylistDownloadNotification(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(getString("playlist_downloading") + " «" + playlistName + "»") + .setContentText(playlistName) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setAutoCancel(true) + .setProgress(100, 0, false); + notificationManager.notify(id, notificationBuilder.build()); + return notificationBuilder; + } + + public static NotificationCompat.Builder buildAllAudiosDownloadNotification(int id) { + var notificationBuilder = new NotificationCompat.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_PLAYLIST_DOWNLOAD_CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(getString("notf_audio_dl_user") + " " + AccountManagerUtils.getUsername()) + .setContentText(getString("all_audio")) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setAutoCancel(true) + .setProgress(100, 0, false); + notificationManager.notify(id, notificationBuilder.build()); + return notificationBuilder; + } + + public 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(getString("cached_track") + ": " + track.toString()); + + notificationManager.notify(track.y1().hashCode(), builder.build()); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java index b3b9883a8..073b7b615 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/MediaFragment.java @@ -39,22 +39,22 @@ import org.json.JSONObject; import java.io.IOException; import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; +import ru.vtosters.lite.downloaders.AudioDownloader; import ru.vtosters.lite.downloaders.VideoDownloader; import ru.vtosters.lite.music.Scrobbler; +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter; import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.LifecycleUtils; public class MediaFragment extends MaterialPreferenceToolbarFragment { - private final int REQUEST_CODE_SET_DOWNLOAD_DIRECTORY = 665; - private final int REQUEST_CODE_SET_MUSIC_DIRECTORY = 666; - private final int REQUEST_CODE_SET_PHOTOS_DIRECTORY = 667; - private final int REQUEST_CODE_SET_VIDEOS_DIRECTORY = 668; - + private static final ExecutorService executor = Executors.newCachedThreadPool(); @Override public void onCreate(Bundle bundle) { @@ -95,6 +95,21 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { findPreference("lastfm_enabled").setEnabled(false); } + findPreference("cached_tracks").setSummary(String.format(AndroidUtils.getString("cached_tracks_counter"), CacheDatabaseDelegate.getTrackCount())); + findPreference("cached_tracks").setOnPreferenceClickListener(preference -> { + if (CacheDatabaseDelegate.getTrackCount() == 0){ + sendToast(AndroidUtils.getString("no_cache_error")); + } else { + delcache(requireContext()); + } + return true; + }); + + findPreference("audio_download").setOnPreferenceClickListener(preference -> { + dlaudio(requireContext()); + return true; + }); + if (!isVkxInstalled()){ findPreference("vkx_sett").setVisible(false); } @@ -178,7 +193,39 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { alert.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(getResources().getColor(com.vtosters.lite.R.color.red)); } - public static void deleteVideoHistory() { + private void delcache(Context ctx) { + VkAlertDialog.Builder alertDialog = new VkAlertDialog.Builder(ctx); + alertDialog.setTitle(AndroidUtils.getString("warning")); + alertDialog.setMessage(AndroidUtils.getString("cached_tracks_remove_confirm")); + alertDialog.setPositiveButton(AndroidUtils.getString("yes"), (dialog, which) -> { + executor.submit(CacheDatabaseDelegate::clear); + }); + alertDialog.setNeutralButton(AndroidUtils.getString("no"), (dialog, which) -> { + dialog.cancel(); + }); + var alert = alertDialog.create(); + alert.show(); + alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); + alert.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(getResources().getColor(com.vtosters.lite.R.color.red)); + } + + private void dlaudio(Context ctx) { + VkAlertDialog.Builder alertDialog = new VkAlertDialog.Builder(ctx); + alertDialog.setTitle(AndroidUtils.getString("download_method")); + alertDialog.setMessage(AndroidUtils.getString("download_method_desc")); + alertDialog.setPositiveButton(AndroidUtils.getString("download_method_cache"), (dialog, which) -> { + executor.submit(AudioDownloader::cacheAllAudios); + }); + alertDialog.setNegativeButton(AndroidUtils.getString("download_method_mp3"), (dialog, which) -> { + executor.submit(AudioDownloader::downloadAllAudios); + }); + var alert = alertDialog.create(); + alert.show(); + alert.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(getAccentColor()); + alert.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(getAccentColor());; + } + + public void deleteVideoHistory() { Thread thread = new Thread(() -> { try { var request = new Request.a() @@ -190,9 +237,9 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { var response = new JSONObject(new OkHttpClient().a(request).execute().a().g()); if (response.optInt("response") == 1){ - LifecycleUtils.getCurrentActivity().runOnUiThread(() -> sendToast(AndroidUtils.getString("video_history_cleaned"))); + requireActivity().runOnUiThread(() -> sendToast(AndroidUtils.getString("video_history_cleaned"))); } else { - LifecycleUtils.getCurrentActivity().runOnUiThread(() -> sendToast(AndroidUtils.getString("delete_video_history_error"))); + requireActivity().runOnUiThread(() -> sendToast(AndroidUtils.getString("delete_video_history_error"))); } Log.d("VideoHistory", response.toString()); diff --git a/app/src/main/java/ru/vtosters/lite/utils/ContactsUtils.java b/app/src/main/java/ru/vtosters/lite/utils/ContactsUtils.java index 8e028a677..0620b369f 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/ContactsUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/ContactsUtils.java @@ -8,6 +8,7 @@ import static ru.vtosters.lite.utils.AndroidUtils.getString; import static ru.vtosters.lite.utils.AndroidUtils.sendToast; import android.Manifest; +import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.SyncResult; @@ -45,11 +46,13 @@ public class ContactsUtils{ }); } - public static void uploadContacts(Context ctx){ + public static void uploadContacts(Activity ctx){ try { if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { sendToast(getString("contact_permission_toast")); requestPermissions(LifecycleUtils.getCurrentActivity(), new String[] {Manifest.permission.READ_CONTACTS}, 11111); + sendToast(getString("contact_permission_toast")); + requestPermissions(ctx, new String[] {Manifest.permission.READ_CONTACTS}, 11111); } else { var account = VKAuthUtils.a.a(VKAccountManager.d().Z()); ContactsSyncAdapterService.c(ctx, account, new Bundle(), new SyncResult()); diff --git a/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java b/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java index d736f16e0..7894817fd 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java @@ -117,4 +117,8 @@ public class IOUtils { return size; } + + public static String getValidFileName(String fileName) { + return fileName.replaceAll("[\\\\/:*?\"<>|]", ""); + } } diff --git a/app/src/main/java/ru/vtosters/lite/utils/LifecycleUtils.java b/app/src/main/java/ru/vtosters/lite/utils/LifecycleUtils.java index 9f9d1486b..bad406545 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/LifecycleUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/LifecycleUtils.java @@ -43,9 +43,7 @@ public class LifecycleUtils { @Override // android.app.Application.ActivityLifecycleCallbacks public void onActivityCreated(Activity activity, Bundle bundle) { - if (activity instanceof VKActivity) { - activities.add(activity); - } + activities.add(activity); } @Override // android.app.Application.ActivityLifecycleCallbacks @@ -56,7 +54,7 @@ public class LifecycleUtils { } public static Activity getCurrentActivity() { - return activities.size() > 0 ? activities.get(0) : null; + return activities.size() > 0 ? activities.get(activities.size() - 1) : null; } public static void restartApplication() { diff --git a/app/src/main/java/ru/vtosters/lite/utils/Preferences.java b/app/src/main/java/ru/vtosters/lite/utils/Preferences.java index d8cbf77ce..40f002988 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/Preferences.java +++ b/app/src/main/java/ru/vtosters/lite/utils/Preferences.java @@ -104,6 +104,10 @@ public class Preferences { return getBoolValue("__dbg_no_slider_ads", true); } + public static boolean autocache() { + return getBoolValue("autocache", false); + } + public static boolean adsstories() { return getBoolValue("adsstories", true); } diff --git a/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java b/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java index 56dc058bb..68c095561 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java @@ -7,6 +7,7 @@ import static ru.vtosters.lite.utils.AndroidUtils.getCenterScreenCoords; import static ru.vtosters.lite.utils.AndroidUtils.getIdentifier; import static ru.vtosters.lite.utils.AndroidUtils.getPrefsValue; import static ru.vtosters.lite.utils.AndroidUtils.getResources; +import static ru.vtosters.lite.utils.LifecycleUtils.*; import static ru.vtosters.lite.utils.Preferences.color_grishka; import static ru.vtosters.lite.utils.Preferences.dockbar_accent; import static ru.vtosters.lite.utils.Preferences.milkshake; @@ -57,7 +58,11 @@ public class ThemesUtils { } // Apply VKTheme and ImTheme (hard applying without dynamic theme changing) public static void setTheme(VKTheme theme, Activity activity) { - if (activity == null) activity = LifecycleUtils.getCurrentActivity(); + if (activity == null) { + activity = getCurrentActivity(); + + if (activity == null) return; + } VKThemeHelper.theme(theme, activity, getCenterScreenCoords()); ThemeTracker.a(); isLoaded = false; @@ -66,7 +71,11 @@ public class ThemesUtils { } // apply changed theme public static void setThemeFL(VKTheme theme, Activity activity, float[] fl) { - if (activity == null) activity = LifecycleUtils.getCurrentActivity(); + if (activity == null) { + activity = getCurrentActivity(); + + if (activity == null) return; + } VKThemeHelper.theme(theme, activity, fl); ThemeTracker.a(); isLoaded = false; diff --git a/build.gradle b/build.gradle index 8f8a149c6..e470766b0 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/scripts/apktool.jar.REMOVED.git-id b/scripts/apktool.jar.REMOVED.git-id deleted file mode 100644 index 2b4d468c9..000000000 --- a/scripts/apktool.jar.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -2966f42770fa9480b905416ecea04a6c4b401193 \ No newline at end of file diff --git a/scripts/framework/1.apk b/scripts/framework/1.apk new file mode 100755 index 000000000..0840244ca Binary files /dev/null and b/scripts/framework/1.apk differ diff --git a/scripts/framework/1.apk.REMOVED.git-id b/scripts/framework/1.apk.REMOVED.git-id deleted file mode 100644 index a6621eba5..000000000 --- a/scripts/framework/1.apk.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -0840244ca18d071426e1a4cf350bdb0d446ca68f \ No newline at end of file diff --git a/smali/lib/armeabi-v7a/libvoip_armv7-a-neon.so.REMOVED.git-id b/smali/lib/armeabi-v7a/libvoip_armv7-a-neon.so.REMOVED.git-id deleted file mode 100644 index 5c673ecff..000000000 --- a/smali/lib/armeabi-v7a/libvoip_armv7-a-neon.so.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -6278c6512808b17c9470c0b231c40ab1d01ef877 \ No newline at end of file diff --git a/smali/res/values-ru/strings.xml b/smali/res/values-ru/strings.xml index 827486af2..11c829f04 100644 --- a/smali/res/values-ru/strings.xml +++ b/smali/res/values-ru/strings.xml @@ -5953,8 +5953,12 @@ Скачать как mp3 Удалить из кеша - Воспроизвести в VK X + Добавить в кеш + Удалить из VK X кеша + Добавить в кеш VK X + + Воспроизвести в VK X Спасибо тебе за поддержку проекта! Поддержи проект донатом и получи бонусы @@ -6252,4 +6256,116 @@ Не отправлять информацию о батарее Включение этой функции скроет информацию о состоянии вашего устройства в режиме экономии батареи и ВКонтакте будет отправлять вам медиаконтент без сильного сжатия и блюра + + Ошибка при записи трека в БД! + Кешированные песни удалены + Закешированный трек + Кешированные песни + Сохраненные треки + Это - список всех кешированных треков. Данный плейлист НЕ СУЩЕСТВУЕТ в вашем профиле.\n\nПри отсутствии соединения с Интернетом данный плейлист превратится в обычный список треков. + Это кешированный альбом! + Загрузка плейлиста + + Кешированный контент + Похоже, тут пусто + У вас отсутствует кешированный контент + Вы действительно хотите удалить все кешированные песни? + + Включите интеграцию VK X! + Получите доступ к хранилищу треков и возможности включать/кешировать треки из VTLite в VK X.\n\nПодробнее - в Настройки > Медиа контент > VK X + + Загрузка музыки + Прогресс загрузки музыки + + Загрузка плейлиста + Прогресс загрузки плейлиста + + Кеширование трека / обложки + + Всего закешировано %1$d песен + + Автокеширование песен + Автоматически кешировать все воспроизведенные вами песни + + Загружено песен: %1$d из %2$d + + Все аудио + Скачивание всех аудио пользователя + У вас отсутствуют кешированные песни + + Способ скачивания + Выберите способ скачивания всех своих аудио (без плейлистов) + В кеш + Как .mp3 + + Скачивание всех своих аудио + + Ошибка в работе приложения. Перезапустите приложение + + Сохранено как + Вы действительно хотите сбросить настройки? + У вас включена системная тема \nПри переключении темы системная тема будет отключена \n\nВы действительно хотите отключить её? + + Сжимать обои + Значительно ускоряет открытие чатов + + от + ошибка + + Экспорт из VTLite + Экспорт истории чата + Обновил фотографию беседы + Удалил фотографию беседы + Создал беседу + Сменил название беседы + на + Добавил + Исключил + группу + пользователя + Закрепил сообщение + Открепил сообщение + Вступил в беседу по ссылке + Сделал скриншот беседы + + фотография + документ + ссылка + запись на стене + голосовое сообщение + аудиозапись + видеозапись + комментарий на стене + история + подкаст + мини-приложение + плейлист + анонимный + публичный + опрос + товар + + Проголосовало + чел. + + ссылки отсутствуют + + Вы действительно хотите очистить историю видео? + Ошибка при очистке истории. Подробности в логах + + Размер файла превышает лимит 6 MB + + Ошибка установки контактов + Для работы данной функции необходимо выдать разрешение на чтение контактов + Загрузка данных о контактах... + Синхронизация контактов + Статус синхронизации + Включена + Отключена + + Не пытаться получать рекламу + Не запрашивать рекламу и рекомендации\n\nМогут быть проблемы + + Не отправлять информацию о батарее + Включение этой функции скроет информацию о состоянии вашего устройства в режиме экономии батареи и ВКонтакте будет отправлять вам медиаконтент без сильного сжатия и блюра \ No newline at end of file diff --git a/smali/res/values/ids.xml b/smali/res/values/ids.xml index 06709c2b8..276bf1ab1 100644 --- a/smali/res/values/ids.xml +++ b/smali/res/values/ids.xml @@ -3915,4 +3915,8 @@ + + + + diff --git a/smali/res/values/strings.xml b/smali/res/values/strings.xml index df2b70c3a..aebc647a0 100644 --- a/smali/res/values/strings.xml +++ b/smali/res/values/strings.xml @@ -2834,7 +2834,7 @@ or deleted" Map Places Up next - Download complete + Download completed Pause Repeat Resume @@ -6088,6 +6088,10 @@ May look like a fake leaving the online" Download as mp3 Delete from cache + Add to cache + + Delete from VK X cache + Add to VK X cache Play the track in VK X @@ -6390,4 +6394,116 @@ May look like a fake leaving the online" Don\'t send battery information Enabling this feature will hide information about the state of your device in battery saving mode and VK will send you media content without strong compression and blur + + Saved as + Are you sure you want to reset your settings? + You have the system theme enabled \nWhen you switch the theme, the system theme will be disabled \n\nAre you sure you want to disable it? + + Compress wallpaper + Greatly speeds up opening chats + + from + error + + Export from VTLite + Export chat history + Updated the chat photo + Removed the chat photo + Created a chat + Changed the name of the conversation + to + Added + Kicked + group + user + Pinned message + Unpinned message + Joined a chat by link + Took a screenshot of the conversation + + photo + document + link + wall post + voice message + audio + video + wall reply + story + podcast + miniapp + playlist + anonymous + public + poll + product + + Voted + user(s) + + no links + + Are you sure you want to clear your video history? + Error clearing video history. Details in the logs + + File size exceeds 6 MB limit + + Error syncing contacts + For this function to work, you must grant permission to read contacts + Loading contact data... + Sync contacts + Sync status + Enabled + Disabled + + Don\'t try to get ads + Don\'t request ads and recommendations\n\nMay cause problems + + Don\'t send battery information + Enabling this feature will hide information about the state of your device in battery saving mode and VK will send you media content without strong compression and blur + + Error writing track to database! + Cached songs deleted + Cached track + Cached songs + Saved tracks + This is a list of all cached tracks. This playlist DOES NOT EXIST on your profile.\n\nIf there is no Internet connection, this playlist will turn into a regular track list. + This is a cached album! + Playlist loading + + Cached content + Looks like it\'s empty + You have no cached content + Are you sure you want to delete all cached songs? + + Enable VK X integration! + Get access to the track storage and the ability to include/cache tracks from VTLite to VK X.\n\nFor details, go to Settings > Media Content > VK X + + Downloading music + Music download progress + + Downloading a playlist + Playlist download progress + + Caching track / cover art + + Total %1$d song(s) cached + + Autocache songs + Automatically cache all songs you have played + + Downloaded %1$d of %2$d tracks + + All audio + Download all user audio + You have no cached songs + + Download method + Select how you want to download all your audios (without playlists) + To cache + As .mp3 + + Download all your audio + + An application error has occurred. Restart the application diff --git a/smali/res/xml/preferences_media.xml b/smali/res/xml/preferences_media.xml index cb580353a..4257d55fb 100644 --- a/smali/res/xml/preferences_media.xml +++ b/smali/res/xml/preferences_media.xml @@ -48,6 +48,17 @@ android:title="@string/lastfm_enabled_title" /> + + + a(Ljava/lang/Object;Ljava/lang/String;)V + invoke-static/range {p1 .. p1}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->injectIntoExistingCatalog(Lorg/json/JSONObject;)V + invoke-virtual {v0, p1}, Lcom/vk/catalog2/core/CatalogParser;->c(Lorg/json/JSONObject;)Lcom/vk/catalog2/core/api/dto/CatalogResponse; move-result-object p1 diff --git a/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheet.smali b/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheet.smali index 604eb2316..d2d09cdcc 100644 --- a/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheet.smali +++ b/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheet.smali @@ -366,6 +366,8 @@ .line 30 iget v2, v2, Lcom/vk/dto/music/Playlist;->O:I + const v2, 0x0 + if-lez v2, :cond_7 new-instance v2, Lcom/vk/music/bottomsheets/a/MusicAction; @@ -462,6 +464,12 @@ invoke-interface {v1, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z :cond_8 + iget-object v2, v0, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheet;->b:Lcom/vk/dto/music/Playlist; + + invoke-static {v1, v2}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;Lcom/vk/dto/music/Playlist;)Ljava/util/ArrayList; + + move-result-object v1 + return-object v1 .end method diff --git a/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener.smali b/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener.smali index 3b77aec8a..c3497694c 100644 --- a/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener.smali +++ b/smali/smali_classes3/com/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener.smali @@ -132,13 +132,22 @@ .end method .method public a(Lcom/vk/music/bottomsheets/a/MusicAction;Lcom/vk/dto/music/Playlist;)Z - .locals 2 + .locals 4 .line 6 invoke-virtual {p1}, Lcom/vk/music/bottomsheets/a/MusicAction;->a()I move-result p1 + invoke-static {p1, p2}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->injectOnClick(ILcom/vk/dto/music/Playlist;)Z + + move-result v3 + + if-eqz v3, :cond_7 + + goto/16 :goto_0 + + :cond_7 const/4 p2, 0x0 sparse-switch p1, :sswitch_data_0 @@ -153,7 +162,7 @@ iget-object v0, p0, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheetClickListener;->e:Lcom/vk/dto/music/Playlist; - invoke-interface {p1, p2, v0}, Lcom/vk/music/bottomsheets/playlist/PlaylistBottomSheetModel;->a(Landroid/content/Context;Lcom/vk/dto/music/Playlist;)V + invoke-static/range {v0 .. v0}, Lru/vtosters/lite/music/cache/injectors/PlaylistInjector;->injectDownloadPlaylist(Lcom/vk/dto/music/Playlist;)V goto/16 :goto_0 diff --git a/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetClickListener.smali b/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetClickListener.smali index 1ef5b1d17..da5ddc1f6 100644 --- a/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetClickListener.smali +++ b/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetClickListener.smali @@ -716,7 +716,7 @@ move-result-object v3 - invoke-static {p1, v0, v2, v3}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->tryPlayInVKX(ILcom/vk/dto/music/MusicTrack;Lcom/vk/music/common/MusicPlaybackLaunchContext;Lcom/vk/dto/music/Playlist;)Z + invoke-static {p1, v0, v2, v3}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->injectOnClick(ILcom/vk/dto/music/MusicTrack;Lcom/vk/music/common/MusicPlaybackLaunchContext;Lcom/vk/dto/music/Playlist;)Z move-result v3 diff --git a/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetModel.smali b/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetModel.smali index d651476ff..bbf6a8478 100644 --- a/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetModel.smali +++ b/smali/smali_classes3/com/vk/music/bottomsheets/track/MusicTrackBottomSheetModel.smali @@ -569,7 +569,7 @@ .method public a(Landroid/content/Context;Lcom/vk/dto/music/MusicTrack;Lcom/vk/music/common/BoomModel$From;Lcom/vk/music/common/MusicPlaybackLaunchContext;)V .locals 1 - invoke-static {p2}, Lru/vtosters/lite/downloaders/AudioDownloader;->downloadAudio(Lcom/vk/dto/music/MusicTrack;)V + invoke-static {p2}, Lru/vtosters/lite/downloaders/AudioDownloader;->cacheTrack(Lcom/vk/dto/music/MusicTrack;)V return-void diff --git a/smali/smali_classes3/com/vk/music/bottomsheets/track/d/VkMusicTrackBottomSheetActionsFactory.smali b/smali/smali_classes3/com/vk/music/bottomsheets/track/d/VkMusicTrackBottomSheetActionsFactory.smali index e7c1202bc..14a728637 100644 --- a/smali/smali_classes3/com/vk/music/bottomsheets/track/d/VkMusicTrackBottomSheetActionsFactory.smali +++ b/smali/smali_classes3/com/vk/music/bottomsheets/track/d/VkMusicTrackBottomSheetActionsFactory.smali @@ -62,52 +62,60 @@ if-nez v2, :cond_4 - .line 4 - new-instance v2, Lcom/vk/music/bottomsheets/a/MusicAction; + # .line 4 + # new-instance v2, Lcom/vk/music/bottomsheets/a/MusicAction; - const v4, 0x7f0a087c + # const v4, 0x7f0a087c - const v3, 0x7f08043c + # const v3, 0x7f08043c - const v5, 0x7f08043a + # const v5, 0x7f08043a - invoke-virtual {v0, v3, v5}, Lcom/vk/music/bottomsheets/track/d/VkMusicTrackBottomSheetActionsFactory;->a(II)I + # invoke-virtual {v0, v3, v5}, Lcom/vk/music/bottomsheets/track/d/VkMusicTrackBottomSheetActionsFactory;->a(II)I - move-result v5 + # move-result v5 - const/4 v6, 0x0 + # const/4 v6, 0x0 - invoke-virtual/range {p1 .. p1}, Lcom/vk/dto/music/MusicTrack;->F1()Z + # invoke-virtual/range {p1 .. p1}, Lcom/vk/dto/music/MusicTrack;->F1()Z - move-result v3 + # move-result v3 - const v12, 0x7f060156 + # const v12, 0x7f060156 - const v13, 0x7f060077 + # const v13, 0x7f060077 - if-eqz v3, :cond_0 + # if-eqz v3, :cond_0 - const v7, 0x7f060156 + # const v7, 0x7f060156 - goto :goto_0 + # goto :goto_0 - :cond_0 - const v7, 0x7f060077 + # :cond_0 + # const v7, 0x7f060077 - :goto_0 - const v8, 0x7f12087d + # :goto_0 + # const v8, 0x7f12087d - const/4 v9, 0x0 + # const/4 v9, 0x0 - const/16 v10, 0x20 + # const/16 v10, 0x20 - const/4 v11, 0x0 + # const/4 v11, 0x0 - move-object v3, v2 + # move-object v3, v2 - invoke-direct/range {v3 .. v11}, Lcom/vk/music/bottomsheets/a/MusicAction;->(IIIIIZILkotlin/jvm/internal/DefaultConstructorMarker;)V + # invoke-direct/range {v3 .. v11}, Lcom/vk/music/bottomsheets/a/MusicAction;->(IIIIIZILkotlin/jvm/internal/DefaultConstructorMarker;)V - invoke-interface {v1, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z + # invoke-interface {v1, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z + + move-object/from16 v10, p1 + + invoke-static {v1, v10}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->hookDownloadBTN(Ljava/util/ArrayList;Lcom/vk/dto/music/MusicTrack;)Ljava/util/ArrayList; + + move-result-object v1 + + move-object/from16 p1, v10 .line 5 sget-object v2, Lcom/vk/music/common/Music$a;->j:Lcom/vk/music/common/Music$a; @@ -697,6 +705,10 @@ .line 31 :cond_9 + invoke-static {v3, v1}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;Lcom/vk/dto/music/MusicTrack;)Ljava/util/ArrayList; + + move-result-object v3 + invoke-interface/range {p2 .. p2}, Lcom/vk/music/bottomsheets/track/MusicTrackBottomSheetModel1;->k()Z move-result v2 @@ -738,11 +750,5 @@ :cond_a :goto_3 - - # invoke static ru.vtosters.lite.hooks.ui.MusicBottomSheetHook.hook(ArrayList actions) - invoke-static {v3}, Lru/vtosters/lite/hooks/ui/MusicBottomSheetHook;->hook(Ljava/util/ArrayList;)Ljava/util/ArrayList; - - move-result-object v3 - return-object v3 .end method diff --git a/smali/smali_classes3/com/vk/music/e/MusicCatalogConfiguration.smali b/smali/smali_classes3/com/vk/music/e/MusicCatalogConfiguration.smali index c5602966e..e8c1f541d 100644 --- a/smali/smali_classes3/com/vk/music/e/MusicCatalogConfiguration.smali +++ b/smali/smali_classes3/com/vk/music/e/MusicCatalogConfiguration.smali @@ -65,6 +65,23 @@ } .end annotation + invoke-static {}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->eligibleForOfflineCaching()Z + + move-result v0 + + if-eqz v0, :cond_custom + + invoke-virtual {p0}, Lcom/vk/catalog2/core/VkCatalogConfiguration;->j()Lcom/vk/catalog2/core/CatalogParser; + + move-result-object v0 + + invoke-static {v0}, Lru/vtosters/lite/music/cache/injectors/TracklistInjector;->createOfflineRx(Lcom/vk/catalog2/core/CatalogParser;)Lio/reactivex/Observable; + + move-result-object v0 + + return-object v0 + + :cond_custom .line 1 new-instance v0, Lcom/vk/catalog2/core/api/music/CatalogGetAudio; diff --git a/smali/smali_classes3/com/vk/music/model/v/MusicTrackModelImpl.smali b/smali/smali_classes3/com/vk/music/model/v/MusicTrackModelImpl.smali index 80d093ffe..c97bacc35 100644 --- a/smali/smali_classes3/com/vk/music/model/v/MusicTrackModelImpl.smali +++ b/smali/smali_classes3/com/vk/music/model/v/MusicTrackModelImpl.smali @@ -211,8 +211,7 @@ if-eqz v1, :cond_0 .line 12 - - invoke-static {p1}, Lru/vtosters/lite/downloaders/AudioDownloader;->downloadAudio(Lcom/vk/dto/music/MusicTrack;)V + invoke-static {p1}, Lru/vtosters/lite/downloaders/AudioDownloader;->cacheTrack(Lcom/vk/dto/music/MusicTrack;)V return-void diff --git a/smali/smali_classes3/com/vk/music/playlist/ModernPlaylistModel.smali b/smali/smali_classes3/com/vk/music/playlist/ModernPlaylistModel.smali index 1dc8f5ad7..a67da6df5 100644 --- a/smali/smali_classes3/com/vk/music/playlist/ModernPlaylistModel.smali +++ b/smali/smali_classes3/com/vk/music/playlist/ModernPlaylistModel.smali @@ -239,11 +239,22 @@ const/4 v1, 0x1 - .line 14 + invoke-static {p1}, Lru/vtosters/lite/music/cache/injectors/PlaylistInjector;->injectGetPlaylist(Lcom/vk/api/audio/AudioGetPlaylist;)Lio/reactivex/Observable; + + move-result-object v2 + + if-eqz v2, :cond_custom + + move-object p1, v2 + + goto :goto_20 + + :cond_custom invoke-static {p1, v0, v1, v0}, Lcom/vk/api/base/ApiRequest;->d(Lcom/vk/api/base/ApiRequest;Lcom/vk/api/base/ApiThreadHolder;ILjava/lang/Object;)Lio/reactivex/Observable; move-result-object p1 + :goto_20 .line 15 new-instance v6, Lcom/vk/music/playlist/ModernPlaylistModel$d; diff --git a/smali/smali_classes3/com/vk/music/playlist/modern/MusicPlaylistContract4.smali b/smali/smali_classes3/com/vk/music/playlist/modern/MusicPlaylistContract4.smali index 02bb4e10d..c317eaef5 100644 --- a/smali/smali_classes3/com/vk/music/playlist/modern/MusicPlaylistContract4.smali +++ b/smali/smali_classes3/com/vk/music/playlist/modern/MusicPlaylistContract4.smali @@ -593,6 +593,10 @@ move-result-object v0 + invoke-static/range {v0 .. v0}, Lru/vtosters/lite/music/cache/injectors/PlaylistInjector;->injectDownloadPlaylist(Lcom/vk/dto/music/Playlist;)V + + return-void + if-eqz v0, :cond_1 .line 3 diff --git a/smali/smali_classes4/com/vtosters/lite/audio/player/CacheUtils.smali b/smali/smali_classes4/com/vtosters/lite/audio/player/CacheUtils.smali index b272e291f..d76705eac 100644 --- a/smali/smali_classes4/com/vtosters/lite/audio/player/CacheUtils.smali +++ b/smali/smali_classes4/com/vtosters/lite/audio/player/CacheUtils.smali @@ -7,7 +7,6 @@ .method public static a(Landroid/content/Context;)Ljava/io/File; .locals 10 - .line 5 invoke-static {p0}, Landroidx/core/content/ContextCompat;->getExternalCacheDirs(Landroid/content/Context;)[Ljava/io/File; move-result-object v0 @@ -111,8 +110,22 @@ .end method .method static a(Ljava/lang/String;)Ljava/io/File; - .locals 4 + .locals 6 + invoke-static {p0}, Lru/vtosters/lite/music/cache/FileCacheImplementation;->isTrackExist(Ljava/lang/String;)Z + + move-result v5 + + if-eqz v5, :cond_228 + + .line 5 + invoke-static {p0}, Lru/vtosters/lite/music/cache/FileCacheImplementation;->getTrackFile(Ljava/lang/String;)Ljava/io/File; + + move-result-object v0 + + return-object v0 + + :cond_228 const/4 v0, 0x0 .line 26 diff --git a/smali/smali_classes4/com/vtosters/lite/audio/utils/Utils.smali b/smali/smali_classes4/com/vtosters/lite/audio/utils/Utils.smali index e361cdfc3..6ae3c2f6a 100644 --- a/smali/smali_classes4/com/vtosters/lite/audio/utils/Utils.smali +++ b/smali/smali_classes4/com/vtosters/lite/audio/utils/Utils.smali @@ -452,6 +452,8 @@ .method public static a([BIIJ)V .locals 10 + return-void + move-wide v0, p3 move p3, p1