beta stuff for music

This commit is contained in:
Vologhat
2022-07-31 22:14:49 +03:00
parent f83e97404a
commit f6555f49d8
33 changed files with 1076 additions and 27 deletions
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
ea0425fab4379ae5f7e9db05a22d4b7afd709560
Binary file not shown.
@@ -1 +0,0 @@
3b92d8876dd59d5afc60a53bb23b103cede6a194
Binary file not shown.
Binary file not shown.
@@ -19,7 +19,7 @@ import ru.vtosters.lite.music.MP3Downloader;
import ru.vtosters.lite.utils.AndroidUtils;
public class AudioDownloader {
private static NotificationManagerCompat notificationManager = NotificationManagerCompat.from(AndroidUtils.getGlobalContext());
private static NotificationManagerCompat notificationManager = NotificationChannels.getNotificationManager();
public static void downloadAudio(MusicTrack track) {
if (AndroidUtils.getDefaultPrefs().getBoolean("new_music_downloading_way", false)) {
@@ -93,7 +93,7 @@ public class AudioDownloader {
}
private static void downloadMP3(MusicTrack track) {
MP3Downloader.execute(track);
MP3Downloader.downloadMP3(track);
}
private static NotificationCompat.Builder buildDownloadNotification(MusicTrack track, int id) {
@@ -103,8 +103,8 @@ public class AudioDownloader {
.setContentText(track.toString())
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setAutoCancel(true);
notificationBuilder.setProgress(100, 0, false);
.setAutoCancel(true)
.setProgress(100, 0, false);
notificationManager.notify(id, notificationBuilder.build());
return notificationBuilder;
}
@@ -13,8 +13,11 @@ import ru.vtosters.lite.utils.AndroidUtils;
public class NotificationChannels {
public static final String MUSIC_DOWNLOAD_CHANNEL_ID = "music_download_channel";
public static final String MUSIC_DOWNLOAD_CHANNEL_NAME = "Загрузка музыки";
public static final String MUSIC_CACHING_NAME = "Кеширование трека / Обложки";
public static final String MUSIC_DOWNLOAD_CHANNEL_DESCRIPTION = "Прогресс загрузки музыки";
private static NotificationManagerCompat notificationManagerCompat;
@RequiresApi(api = Build.VERSION_CODES.O)
public static void createChannels() {
createMusicDownloadChannel();
@@ -29,4 +32,10 @@ public class NotificationChannels {
var notificationManager = NotificationManagerCompat.from(AndroidUtils.getGlobalContext());
notificationManager.createNotificationChannel(musicDownloadChannel);
}
public static NotificationManagerCompat getNotificationManager() {
if (notificationManagerCompat == null)
notificationManagerCompat = NotificationManagerCompat.from(AndroidUtils.getGlobalContext());
return notificationManagerCompat;
}
}
@@ -0,0 +1,61 @@
package ru.vtosters.lite.music;
import android.app.Notification;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URL;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import ru.vtosters.lite.utils.IOUtils;
public class ArtworkDownloader {
public static void downloadArtworks(MusicTrack track) throws IOException {
var obj = track.J();
if (!obj.has("album")) return;
JSONObject album = obj.optJSONObject("album");
if (!album.has("thumb")) return;
JSONObject thumb = album.optJSONObject("thumb");
var trackId = track.y1();
if (thumb.has("photo_300"))
downloadArtwork(thumb.optString("photo_300"), 300, trackId.hashCode(), trackId);
if (thumb.has("photo_600"))
downloadArtwork(thumb.optString("photo_600"), 600, trackId.hashCode(), trackId);
if (thumb.has("photo_1200"))
downloadArtwork(thumb.optString("photo_1200"), 1200, trackId.hashCode(), trackId);
}
private static void downloadArtwork(String url, int res, int hash, String trackId) throws IOException {
var content = IOUtils.readAllBytes(new URL(url).openStream());
IOUtils.writeToFile(FileCacheImplementation.getTrackArtwork(trackId, res), content);
Log.d("ArtworkDownloader", "Downloading " + url);
//notifyArtwork(AndroidUtils.getGlobalContext(), hash, res);
}
private static void notifyArtwork(Context ctx, int hash, int resolution) {
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= 26) {
builder = new Notification.Builder(ctx, NotificationChannels.MUSIC_DOWNLOAD_CHANNEL_ID);
builder.setVisibility(Notification.VISIBILITY_PUBLIC);
} else builder = new Notification.Builder(ctx);
builder.setSmallIcon(com.vtosters.lite.R.drawable.ic_download_outline_28)
.setShowWhen(false)
.setContentTitle(NotificationChannels.MUSIC_CACHING_NAME)
.setContentText("Разрешение обложки: " + resolution + "x" + resolution);
NotificationChannels.getNotificationManager().notify(hash, builder.build());
}
}
@@ -1,17 +1,23 @@
package ru.vtosters.lite.music;
import android.app.DownloadManager;
import android.app.Notification;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URL;
import java.util.stream.Collectors;
import okhttp3.Call;
import okhttp3.Callback;
@@ -19,19 +25,86 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
public class MP3Downloader {
public final static int CACHE_STRATEGY = 0;
public final static int EXTERNAL_STORAGE_STRATEGY = 1;
private static final String URL_FORMAT = "https://api.vk.com/method/audio.getById?v=5.131&access_token=%s&audios=%s&copyright=https://vtosters.app/";
private static final OkHttpClient client = new OkHttpClient();
public static void execute(MusicTrack track) {
private static int getSelectedStrategy() {
return AndroidUtils.getPreferences().getInt("music_storing_strategy", CACHE_STRATEGY);
}
public static void downloadPlaylist(Playlist playlist) {
var token = AndroidUtils.getPreferences().getString("vk_admin_token", "");
if (token.isEmpty()) {
Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show();
return;
}
String trackIds = playlist.R
.stream()
.map(MusicTrack::y1)
.collect(Collectors.joining(","));
var request = new Request.a()
.b(String.format(URL_FORMAT, token, trackIds))
.a();
client.a(request).a(new Callback() {
@Override
public void a(Call call, IOException e) {
Log.d("MP3Downloader", e + "");
}
@Override
public void a(Call call, Response response) throws IOException {
var payload = response.a().g();
Log.d("MP3Downloader", payload);
try {
JSONObject jsonObj = new JSONObject(payload);
if (!jsonObj.has("response"))
return;
var tracks = new JSONObject(payload).getJSONArray("response");
for (int i = 0; i < tracks.length(); i++) {
JSONObject responseNode = tracks.getJSONObject(i);
if (!responseNode.has("url"))
continue;
var url = responseNode.optString("url");
Log.d("MP3Downloader", url);
if (TextUtils.isEmpty(url))
return;
var track = playlist.R.get(i);
switch (getSelectedStrategy()) {
case CACHE_STRATEGY:
saveToCache(track, url);
break;
case EXTERNAL_STORAGE_STRATEGY:
saveToDownloads(track.toString(), url);
}
}
} catch (JSONException e) {
Log.d("MP3Downloader", e + "");
}
}
});
}
public static void downloadMP3(MusicTrack track) {
var token = AndroidUtils.getPreferences().getString("vk_admin_token", "");
if (token.isEmpty()) {
Toast.makeText(AndroidUtils.getGlobalContext(), "Обновите токен VK Admin в настройках", Toast.LENGTH_SHORT).show();
return;
}
var request = new Request.a()
.b(String.format(URL_FORMAT, token, track.y1()))
.a();
@@ -47,20 +120,28 @@ public class MP3Downloader {
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 != null) {
if (url.endsWith(".mp3")) {
downloadMP3(track.toString(), url);
} else if (VKM3UParser.isM3U8(url)) {
AudioDownloader.downloadM3U8(track);
}
}
}
if (!jsonObj.has("response"))
return;
JSONObject responseNode = new JSONObject(payload).getJSONArray("response")
.getJSONObject(0);
if (!responseNode.has("url"))
return;
var url = responseNode.optString("url");
Log.d("MP3Downloader", url);
if (TextUtils.isEmpty(url))
return;
if (VKM3UParser.isM3U8(url)) {
AudioDownloader.downloadM3U8(track);
return;
}
switch (getSelectedStrategy()) {
case CACHE_STRATEGY:
ArtworkDownloader.downloadArtworks(track);
saveToCache(track, url);
break;
case EXTERNAL_STORAGE_STRATEGY:
saveToDownloads(track.toString(), url);
break;
}
} catch (JSONException e) {
Log.d("MP3Downloader", e + "");
@@ -69,7 +150,37 @@ public class MP3Downloader {
});
}
private static void downloadMP3(String title, String url) {
private static void saveToCache(MusicTrack track, String url) throws IOException {
var trackId = track.y1();
if (CacheDatabaseDelegate.isCached(trackId))
return;
var content = IOUtils.readAllBytes(new URL(url).openStream());
var trackFile = FileCacheImplementation.getTrackFile(trackId);
if (!trackFile.exists())
trackFile.getParentFile().mkdirs();
IOUtils.writeToFile(trackFile, content);
notifySavingToCache(track);
CacheDatabaseDelegate.insertTrack(track);
NotificationChannels.getNotificationManager().cancel(trackId.hashCode());
}
private static void notifySavingToCache(MusicTrack track) {
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= 26) {
builder = new Notification.Builder(AndroidUtils.getGlobalContext(), NotificationChannels.MUSIC_DOWNLOAD_CHANNEL_ID);
builder.setVisibility(Notification.VISIBILITY_PUBLIC);
} else builder = new Notification.Builder(AndroidUtils.getGlobalContext());
builder.setSmallIcon(com.vtosters.lite.R.drawable.ic_download_outline_28)
.setShowWhen(false)
.setContentTitle("Закешированный трек: " + track.toString());
NotificationChannels.getNotificationManager().notify(track.y1().hashCode(), builder.build());
}
private static void saveToDownloads(String title, String url) {
var downloadManager = AndroidUtils.getGlobalContext().getSystemService(DownloadManager.class);
var request = new DownloadManager.Request(Uri.parse(url))
.setAllowedOverRoaming(true)
@@ -0,0 +1,346 @@
package ru.vtosters.lite.music.cache;
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;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
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(AndroidUtils.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("Ошибка при записи трека в БД! [" + e.getMessage() + "]", true);
}
}
public static void clear() {
getHelper().getReadableDatabase().execSQL(String.format("DELETE FROM %s", TABLE_NAME));
}
@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[0]);
var ownerId = Integer.parseInt(splits[1]);
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.getTrackArtwork(trackId, 300).exists()
&& !FileCacheImplementation.getTrackArtwork(trackId, 600).exists()
&& !FileCacheImplementation.getTrackArtwork(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.getTrackArtwork(trackId, 300)))
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackArtwork(trackId, 600)))
.putOpt("photo_1200", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackArtwork(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 LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<Boolean>() {
@Override
public Boolean run(ILibVkxService service) {
try {
return service.getCache().size() > 0;
} catch (RemoteException e) {
return false;
}
}
@Override
public Boolean defaultValue() {
return false;
}
});
}
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<MusicTrack> getTracks() {
return getTracks(String.format(
"SELECT * FROM %s",
TABLE_NAME
));
}
public static List<MusicTrack> 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<MusicTrack> 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<MusicTrack> getTracksByAlbumOne(String id) {
return getTracks(String.format(
"SELECT * FROM %s WHERE %s = %s LIMIT 1",
TABLE_NAME,
COLUMN_ALBUM_ID,
id
));
}
public static List<MusicTrack> getTracks(String query) {
List<MusicTrack> 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) {
if (!LibVKXClient.isIntegrationEnabled()) {
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;
}
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<Boolean>() {
@Override
public Boolean run(ILibVkxService service) {
try {
var splits = trackId.split("_");
var id = Integer.parseInt(splits[0]);
var owner = Integer.parseInt(splits[1]);
return service.isTrackCached(id, owner);
} catch (RemoteException e) {
return false;
}
}
@Override
public Boolean defaultValue() {
return false;
}
});
}
public static void removeTrackFromCache(String trackId) {
if (LibVKXClient.isIntegrationEnabled()) {
LibVKXClient.getInstance().runOnService((service -> {
try {
var splits = trackId.split("_");
var id = Integer.parseInt(splits[0]);
var owner = Integer.parseInt(splits[1]);
service.deleteTrackFromCache(id, owner);
} catch (RemoteException e) {
e.printStackTrace();
}
}));
return;
}
IOUtils.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<Long>() {
@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) {
}
}
}
@@ -0,0 +1,48 @@
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) {
return new File(getCacheDir(), trackId);
}
public static File getTrackFile(String trackId) {
return new File(getTrackFolder(trackId), "track.mp3");
}
public static File getTrackFile(MusicTrack track) {
return getTrackFile(track.y1());
}
public static File getTrackArtwork(String trackId, int factor) {
return new File(getTrackFolder(trackId), "artwork_" + 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";
}
}
@@ -0,0 +1,27 @@
package ru.vtosters.lite.music.cache;
import com.vk.dto.common.data.VKList;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import ru.vtosters.lite.utils.ReflectionUtils;
public class VKListHelper {
public static void addToVKList(VKList<?> vkList, Object addition) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ReflectionUtils.invokeStaticMethod(VKList.class, "add", new Class[]{Object.class}, addition);
}
public static void addToVKList(VKList<?> vkList, int index, Object addition) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ReflectionUtils.invokeStaticMethod(VKList.class, "add", new Class[]{int.class, Object.class}, index, addition);
}
public static void addAllToVKList(VKList<?> list, Collection<?> addition) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ReflectionUtils.invokeStaticMethod(VKList.class, "addAll", new Class[]{Collection.class}, addition);
}
public static void addAllToVKList(VKList<?> list, int index, Collection<?> addition) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ReflectionUtils.invokeStaticMethod(VKList.class, "addAll", new Class[]{int.class, Collection.class}, index, addition);
}
}
@@ -0,0 +1,92 @@
package ru.vtosters.lite.music.cache.helpers;
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", "Кешированные треки")
.put("description", "Это - список всех кешированных треков. Данный плейлист НЕ СУЩЕСТВУЕТ в вашем профиле.\n\n" +
"При отсутствии соединения с Интернетом данный плейлист превратится в обычный список треков.")
.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.getTrackArtwork(trackId, 300)))
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackArtwork(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 + "/ Это кешированный альбом!")
.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<Playlist> getAlbumPlaylists() {
List<Playlist> list = new ArrayList<>();
CacheDatabaseDelegate.getTracksAsPlaylist().forEach(track -> {
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;
}
}
@@ -0,0 +1,40 @@
package ru.vtosters.lite.music.cache.helpers;
import com.vk.dto.music.MusicTrack;
import org.json.JSONArray;
import java.util.List;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
public class TracklistHelper {
public static List<MusicTrack> getTracks() {
return CacheDatabaseDelegate.getTracks();
}
public static List<MusicTrack> getTracks(String id) {
return CacheDatabaseDelegate.getTracksByAlbum(id);
}
public static List<MusicTrack> getTracksOne(String id) {
return CacheDatabaseDelegate.getTracksByAlbumOne(id);
}
public static JSONArray tracksToIds(List<MusicTrack> tracks) {
var arr = new JSONArray();
for (MusicTrack track : tracks) {
arr.put(track.y1());
}
return arr;
}
public static JSONArray tracksToJsons(List<MusicTrack> tracks) {
var arr = new JSONArray();
for (MusicTrack track : tracks) {
arr.put(track.J());
}
return arr;
}
}
@@ -0,0 +1,80 @@
package ru.vtosters.lite.music.cache.injectors;
import android.os.RemoteException;
import android.text.TextUtils;
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.music.MP3Downloader;
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) {
MP3Downloader.downloadPlaylist(playlist);
}
public static boolean eligibleForOfflineCaching() {
return !NetworkUtils.isNetworkConnected() && CacheDatabaseDelegate.hasTracks();
}
public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) {
try {
if (!eligibleForOfflineCaching())
return null;
var requestArgs = audioGetPlaylist.b();
var id = requestArgs.get("id");
if (TextUtils.isEmpty(id) || !id.equals("-1") && !id.equals("-2"))
return null;
return Observable.a((ObservableOnSubscribe<AudioGetPlaylist.c>) observableEmitter -> {
AudioGetPlaylist.c response = new AudioGetPlaylist.c();
if (LibVKXClient.isIntegrationEnabled()) {
response.b = PlaylistHelper.createCachedPlaylistMetadata();
LibVKXClient.getInstance().runOnService((service) -> {
try {
List<String> cache = service.getCache();
ArrayList<MusicTrack> tracks = new ArrayList<>();
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 (id.equals("-1")) {
response.c = (ArrayList<MusicTrack>) TracklistHelper.getTracks();
response.b = PlaylistHelper.createCachedPlaylistMetadata();
} else {
response.c = (ArrayList<MusicTrack>) TracklistHelper.getTracks(requestArgs.get("owner_id"));
response.b = PlaylistHelper.createCachedPlaylistMetadata(requestArgs.get("owner_id"));
}
observableEmitter.b(response);
});
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,197 @@
package ru.vtosters.lite.music.cache.injectors;
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.CacheDatabaseDelegate;
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() && CacheDatabaseDelegate.hasTracks();
}
public static Observable<CatalogResponse> createOfflineRx(CatalogParser parser) {
return Observable.a((ObservableOnSubscribe<CatalogResponse>) observableEmitter -> {
if (LibVKXClient.isIntegrationEnabled()) {
LibVKXClient.getInstance().runOnService((service) -> {
try {
List<MusicTrack> tracks = new ArrayList<>();
List<String> cache = service.getCache();
for (String json : cache) {
tracks.add(new MusicTrack(new JSONObject(json)));
}
observableEmitter.b(parser.c(createVirtualCatalog(tracks)));
} catch (Exception e) {
e.printStackTrace();
}
});
} else {
observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks())));
}
});
}
public static void injectIntoExistingCatalog(JSONObject catalogNode) {
if (!CacheDatabaseDelegate.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<MusicTrack> tracks) throws JSONException {
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", "Включите интеграцию VK X!")
.put("text", "Получите доступ к хранилищу треков и возможности включать/кешировать треки из VTosters в VK X.\n\n" +
"Подробнее - в Настройки > Настройки VTosters > Кешированные треки.")
.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", "Сохраненные треки")
.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();
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
2966f42770fa9480b905416ecea04a6c4b401193
BIN
View File
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
0840244ca18d071426e1a4cf350bdb0d446ca68f
Binary file not shown.
@@ -1 +0,0 @@
6278c6512808b17c9470c0b231c40ab1d01ef877
@@ -89,6 +89,8 @@
invoke-static {p1, v1}, Lkotlin/jvm/internal/Intrinsics;->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
@@ -153,7 +153,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
@@ -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;
@@ -239,11 +239,23 @@
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;
@@ -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
@@ -8,6 +8,12 @@
.locals 10
.line 5
invoke-static/range {p1 .. p1}, Lru/vtosters/lite/music/cache/FileCacheImplementation;->getTrackFile(Ljava/lang/String;)Ljava/io/File;
move-result-object v0
return-object v0
invoke-static {p0}, Landroidx/core/content/ContextCompat;->getExternalCacheDirs(Landroid/content/Context;)[Ljava/io/File;
move-result-object v0
@@ -452,6 +452,8 @@
.method public static a([BIIJ)V
.locals 10
return-void
move-wide v0, p3
move p3, p1