From 19fb86ca58be027882eee3c4c4a69167b4d512c4 Mon Sep 17 00:00:00 2001 From: Vologhat Date: Sat, 8 Oct 2022 10:37:47 +0300 Subject: [PATCH] refactored last.fm scrobbler --- .../vtosters/lite/music/LastFMScrobbler.java | 219 ++++++++++++++++++ .../ru/vtosters/lite/music/Scrobbler.java | 213 ----------------- .../lite/ui/fragments/MediaFragment.java | 10 +- .../ru/vtosters/lite/utils/AndroidUtils.java | 36 ++- .../lite/audio/player/ExoPlayerHelper.smali | 2 +- 5 files changed, 242 insertions(+), 238 deletions(-) create mode 100644 app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java delete mode 100644 app/src/main/java/ru/vtosters/lite/music/Scrobbler.java diff --git a/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java b/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java new file mode 100644 index 000000000..72227324a --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/music/LastFMScrobbler.java @@ -0,0 +1,219 @@ +package ru.vtosters.lite.music; + +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.vk.dto.music.MusicTrack; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.FormBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import ru.vtosters.lite.downloaders.AudioDownloader; +import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; +import ru.vtosters.lite.utils.AndroidUtils; +import ru.vtosters.lite.utils.Preferences; + +public class LastFMScrobbler { + private static final String KEY = "5965d63402414776c54c266db0211746"; + private static final String KEY_SECRET = "d982180eed13275bb948e41cf225f88f"; + private static final String URL = "https://ws.audioscrobbler.com/2.0/"; + private static final OkHttpClient CLIENT = new OkHttpClient(); + private static final List SCROBBLED_UUIDS = new ArrayList<>(); + + public static void grabMusicTrack(MusicTrack musictrack) { + var uid = musictrack.y1(); + var artist = musictrack.C; + var title = musictrack.f; + var duration = musictrack.h; + var track_id = musictrack.d; + var owner_id = musictrack.e; + var access_key = !TextUtils.isEmpty(musictrack.J) ? musictrack.J : ""; + var album_id = musictrack.I; + var album = album_id != null ? album_id.getTitle() : null; + + if (TextUtils.isEmpty(uid) || TextUtils.isEmpty(artist) || TextUtils.isEmpty(title) || duration == 0) { + Log.d("Scrobbler", "grabTrackInfo: " + "Empty track, info: " + artist + " - " + title + " - " + duration + " - " + uid); + return; + } + + if (Preferences.autocache() && !CacheDatabaseDelegate.isCached(uid)) { + if (LibVKXClient.isIntegrationEnabled()){ + LibVKXClient.getInstance().runOnService(service -> { + try { + service.addTrackToCache(track_id, owner_id, access_key); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + } else { + AudioDownloader.cacheTrack(musictrack); + } + } + + scrobbleTrack(duration, artist, title, uid, album); + } + + public static void scrobbleTrack( + long duration, + String artist, + String title, + String uid, + @Nullable String album + ) { + Log.d("Scrobbler", "scrobbleTrack: " + title + " - " + artist + " - " + duration + " - " + uid); + + if (!needScrobbling(uid) || !isLoggedIn() || TextUtils.isEmpty(artist) || TextUtils.isEmpty(title) || duration == 0) + return; + + SCROBBLED_UUIDS.add(uid); + + var params = new TreeMap(); + params.put("api_key", KEY); + params.put("method", "track.scrobble"); + params.put("sk", getSessionKey()); + params.put("artist[0]", artist); + params.put("track[0]", title); + if (!TextUtils.isEmpty(album)) + params.put("album[0]", album); + params.put("duration[0]", String.valueOf(duration)); + params.put("timestamp[0]", String.valueOf(System.currentTimeMillis() / 1000)); + + + fetch(params, new Callback() { + @Override + public void a(Call call, IOException e) { + Log.d("Scrobbler", e+""); + } + + @Override + public void a(Call call, Response response) throws IOException { + Log.d("Scrobbler", "Scrobbled: " + artist + " - " + title); + Log.d("Scrobbler", "Response scrobble: " + response.a().g()); + } + }); + } + + public static void authenticate(String username, String password) { + if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { + AndroidUtils.sendToast(AndroidUtils.getString("lastfm_credentials_empty")); + return; + } + + var params = new TreeMap(); + params.put("api_key", KEY); + params.put("method", "auth.getMobileSession"); + params.put("username", username); + params.put("password", password); + + fetch(params, new Callback() { + @Override + public void a(Call call, IOException e) { + Log.d("Scrobbler", "Auth failed", e); + AndroidUtils.sendToast(AndroidUtils.getString("auth_error_vtl")); + } + + @Override + public void a(Call call, Response response) throws IOException { + try { + var resBody = response.a().g(); + var json = new JSONObject(resBody); + fetchSession(json); + Log.d("Scrobbler", "Auth response " + resBody); + } catch (JSONException e) { + Log.d("Scrobbler", "Auth failed", e); + } + } + }); + } + + private static String generateMD5(Map params) { + var sb = new StringBuilder(); + for (var entry : params.entrySet()) + sb.append(entry.getKey()).append(entry.getValue()); + return AndroidUtils.MD5(sb.append(KEY_SECRET).toString()); + } + + private static void fetch(TreeMap params, Callback callback) { + // Each call must be signed with md5. + // For more information check https://www.last.fm/api + params.put("api_sig", generateMD5(params)); + params.put("format", "json"); + + var reqBodyBuilder = new FormBody.a(StandardCharsets.UTF_8); + for (var entry : params.entrySet()) + reqBodyBuilder.a(entry.getKey(), entry.getValue()); + + var req = new Request.a() + .b(URL) + .a(reqBodyBuilder.a()) + .a(); + + CLIENT.a(req).a(callback); + } + + public static void fetchSession(JSONObject json) throws JSONException { + if (!json.has("session")) { + Log.d("Scrobbler", "Auth failed - no session"); + return; + } + + var session = json.getJSONObject("session"); + var name = session.getString("name"); + var key = session.getString("key"); + + AndroidUtils.edit() + .putString("username", name) + .putString("sessionKey", key) + .putBoolean("lastfm_enabled", true) + .apply(); + + Log.d("Scrobbler", "Auth success as " + name + ", key " + key); + } + + public static void logout() { + AndroidUtils.edit() + .remove("username") + .remove("sessionKey") + .putBoolean("lastfm_enabled", false) + .apply(); + SCROBBLED_UUIDS.clear(); + AndroidUtils.sendToast(AndroidUtils.getString("lastfm_logout_success")); + } + + public static boolean needScrobbling(String uid) { + return isLoggedIn() && isScrobblingEnabled() && !SCROBBLED_UUIDS.contains(uid); + } + + public static boolean isLoggedIn() { + return !TextUtils.isEmpty(getSessionKey()); + } + + public static boolean isScrobblingEnabled() { + return AndroidUtils.getPreferences().getBoolean("lastfm_enabled", false); + } + + public static String getUserName() { + return AndroidUtils.getDefaultPrefs().getString("username", null); + } + + private static String getSessionKey() { + return AndroidUtils.getDefaultPrefs().getString("sessionKey", null); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java b/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java deleted file mode 100644 index 1ae20db3c..000000000 --- a/app/src/main/java/ru/vtosters/lite/music/Scrobbler.java +++ /dev/null @@ -1,213 +0,0 @@ -package ru.vtosters.lite.music; - -import static java.lang.String.valueOf; -import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.getInstance; -import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.*; -import static ru.vtosters.lite.utils.AndroidUtils.MD5; -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.os.RemoteException; -import android.util.Log; - -import androidx.annotation.Nullable; - -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.Map; -import java.util.TreeMap; - -import bruhcollective.itaysonlab.libvkx.client.LibVKXClient; -import ru.vtosters.lite.downloaders.AudioDownloader; -import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; -import ru.vtosters.lite.net.NetCall; -import ru.vtosters.lite.net.NetCallback; -import ru.vtosters.lite.net.NetClient; -import ru.vtosters.lite.net.NetRequest; -import ru.vtosters.lite.net.NetResponse; -import ru.vtosters.lite.utils.AndroidUtils; - -public class Scrobbler { - private static final String key = "5965d63402414776c54c266db0211746"; - private static final String keysecret = "d982180eed13275bb948e41cf225f88f"; - private static final String url = "https://ws.audioscrobbler.com/2.0/"; - private static final NetClient client = new NetClient.Builder().build(); - private static final ArrayList scrobbled = new ArrayList<>(); - - public static void scrobbleTrack(long duration, String artist, String title, String uid, @Nullable String album) { - var string = getDefaultPrefs().getString("sessionKey", null); - var timestamp = System.currentTimeMillis() / 1000; - - Log.d("Scrobbler", "scrobbleTrack: " + title + " - " + artist + " - " + duration + " - " + uid); - - if (!needToBeScrobble(uid) || - string == null || - string.isEmpty() || - artist == null || - artist.isEmpty() || - title == null || - title.isEmpty() || - duration == 0) return; - - scrobbled.add(uid); // add to scrobbled list - - TreeMap treeMap = new TreeMap<>(); // create map for request - treeMap.put("method", "track.scrobble"); - treeMap.put("sk", string); - treeMap.put("artist[0]", artist); - treeMap.put("track[0]", title); - if (album != null && !album.isEmpty()) - treeMap.put("album[0]", album); - treeMap.put("timestamp[0]", valueOf(timestamp)); - treeMap.put("api_key", key); - treeMap.put("duration[0]", valueOf(duration)); - treeMap.put("api_sig", signMD5(treeMap)); - treeMap.put("format", "json"); - - client.newCall(new NetRequest.Builder().url(url).post().params(treeMap).build()).enqueue(new NetCallback() { - @Override - public void onFailure(NetCall call, IOException e) { - Log.d("Scrobbler", "Error: " + e.getMessage()); - } - - @Override - public void onResponse(NetCall call, NetResponse response) { - Log.d("Scrobbler", "Scrobbled: " + artist + " - " + title); - Log.d("Scrobbler", "Response scrobble: " + response.getDataString()); - } - }); - } - - public static void grabMusicTrack(MusicTrack musictrack) { - var uid = musictrack.y1(); - var artist = musictrack.C; - var title = musictrack.f; - var duration = musictrack.h; - var track_id = musictrack.d; - var owner_id = musictrack.e; - var access_key = musictrack.J; - var albumid = musictrack.I; - String album; - - if (albumid != null) { - album = albumid.getTitle(); - } else { - album = null; - } - - if (uid.isEmpty() || artist == null || artist.isEmpty() || title == null || title.isEmpty() || duration == 0) { - Log.d("Scrobbler", "grabTrackInfo: " + "Empty track, info: " + artist + " - " + title + " - " + duration + " - " + uid); - return; - } - - if (autocache() && !isCached(uid)) { - if (LibVKXClient.isIntegrationEnabled()){ - getInstance().runOnService(service -> { - try { - service.addTrackToCache(track_id, owner_id, (access_key != null) ? access_key : ""); - } catch (RemoteException e) { - e.printStackTrace(); - } - }); - } else { - AudioDownloader.cacheTrack(musictrack); - } - } - - scrobbleTrack(duration, artist, title, uid, album); - } - - private static String signMD5(Map map) { - var sb = new StringBuilder(); - - for (Map.Entry entry : map.entrySet()) { - sb.append(entry.getKey()); - sb.append(entry.getValue()); - } - - sb.append(keysecret); - - return MD5(sb.toString()); - } - - public static void auth(String login, String pass) { - if (login == null || pass == null) { - sendToast(AndroidUtils.getString("lastfm_credentials_empty")); - return; - } - - TreeMap treeMap = new TreeMap<>(); // create map for request - treeMap.put("method", "auth.getMobileSession"); - treeMap.put("api_key", key); - treeMap.put("username", login); - treeMap.put("password", pass); - treeMap.put("api_sig", signMD5(treeMap)); - treeMap.put("format", "json"); - - client.newCall(new NetRequest.Builder().url(url).post().params(treeMap).build()).enqueue(new NetCallback() { - @Override - public void onFailure(NetCall call, IOException e) { - Log.d("Scrobbler", "Auth failed", e); - sendToast(AndroidUtils.getString("auth_error_vtl")); - } - - @Override - public void onResponse(NetCall call, NetResponse response) { - try { - var jSONObject = new JSONObject(response.getDataString()); - sessionFetcher(jSONObject); - Log.d("Scrobbler", "Auth response " + response.getDataString()); - } catch (JSONException e) { - Log.d("Scrobbler", "Auth failed", e); - } - } - }); - } - - public static void sessionFetcher(JSONObject jsonObject) throws JSONException { - if (!jsonObject.has("session")) { - Log.d("Scrobbler", "Auth failed - no session"); - return; - } - - var session = jsonObject.getJSONObject("session"); - var name = session.getString("name"); - var key = session.getString("key"); - - getDefaultPrefs().edit().putString("username", name). - putString("sessionKey", key).apply(); - - Log.d("Scrobbler", "Auth success as " + name + ", key " + key); - } - - public static void logout() { - edit().remove("username").apply(); - edit().remove("sessionKey").apply(); - scrobbled.clear(); - sendToast(AndroidUtils.getString("lastfm_logout_success")); - } - - public static boolean needToBeScrobble(String uid) { - return isLoggedIn() && isScrobblingEnabled() && !scrobbled.contains(uid); - } - - public static boolean isLoggedIn() { - return getDefaultPrefs().getString("sessionKey", null) != null; - } - - public static boolean isScrobblingEnabled() { - return getPreferences().getBoolean("lastfm_enabled", false); - } - - public static String getUserName() { - return getDefaultPrefs().getString("username", null); - } -} 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 cd4af0eb9..ec3449cb0 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 @@ -1,7 +1,7 @@ package ru.vtosters.lite.ui.fragments; import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.isVkxInstalled; -import static ru.vtosters.lite.music.Scrobbler.isLoggedIn; +import static ru.vtosters.lite.music.LastFMScrobbler.isLoggedIn; import static ru.vtosters.lite.proxy.ProxyUtils.getApi; import static ru.vtosters.lite.utils.AccountManagerUtils.getUserToken; import static ru.vtosters.lite.utils.AndroidUtils.dp2px; @@ -44,7 +44,7 @@ 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.LastFMScrobbler; import ru.vtosters.lite.music.cache.CacheDatabaseDelegate; import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter; import ru.vtosters.lite.utils.AndroidUtils; @@ -107,7 +107,7 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { }); if (isLoggedIn()) { - findPreference("lastfm_auth").setSummary(AndroidUtils.getString("lastfm_authorized_as") + " " + Scrobbler.getUserName()); + findPreference("lastfm_auth").setSummary(AndroidUtils.getString("lastfm_authorized_as") + " " + LastFMScrobbler.getUserName()); } else { findPreference("lastfm_enabled").setEnabled(false); } @@ -181,7 +181,7 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { (dialog, which) -> { String login = fn.getText().toString(); String pass = ln.getText().toString(); - Scrobbler.auth(login, pass); + LastFMScrobbler.authenticate(login, pass); } ) .setView(linearLayout) @@ -193,7 +193,7 @@ public class MediaFragment extends MaterialPreferenceToolbarFragment { .setTitle(AndroidUtils.getString("lastfm_logout_title")) .setMessage(AndroidUtils.getString("lastfm_logout_confirm")) .setPositiveButton(AndroidUtils.getString("vkim_yes"), - (dialog, which) -> Scrobbler.logout()) + (dialog, which) -> LastFMScrobbler.logout()) .setNeutralButton(AndroidUtils.getString("vkim_no"), (dialog, which) -> dialog.cancel()) .show(); diff --git a/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java b/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java index 3fcb53999..7b6810514 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java @@ -1,32 +1,31 @@ package ru.vtosters.lite.utils; -import static ru.vtosters.lite.hooks.DateHook.getLocale; -import static ru.vtosters.lite.utils.Preferences.preferences; - import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import com.vk.core.util.Screen; +import com.vk.core.util.ToastUtils; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; import java.util.Objects; +import ru.vtosters.lite.hooks.DateHook; + public class AndroidUtils { public static SharedPreferences getDefaultPrefs() { - if (preferences == null) - preferences = PreferenceManager.getDefaultSharedPreferences(getGlobalContext()); - return preferences; + if (Preferences.preferences == null) + Preferences.preferences = PreferenceManager.getDefaultSharedPreferences(getGlobalContext()); + return Preferences.preferences; } public static SharedPreferences getPreferences() { @@ -90,7 +89,7 @@ public class AndroidUtils { } public static Context setDefaultLocale(Context context) { - Locale locale = new Locale(getLocale()); + Locale locale = new Locale(DateHook.getLocale()); Locale.setDefault(locale); var resources = context.getResources(); @@ -123,26 +122,25 @@ public class AndroidUtils { } public static void sendToast(String text) { - Toast.makeText(getGlobalContext(), text, Toast.LENGTH_SHORT).show(); + ToastUtils.a(text); } public static String MD5(String s) { try { - var messageDigest = MessageDigest.getInstance("MD5"); + var md = MessageDigest.getInstance("MD5"); + md.update(s.getBytes()); + var md5 = md.digest(); var sb = new StringBuilder(); - messageDigest.update(s.getBytes()); - byte[] digest = messageDigest.digest(); - for (byte b : digest) { - StringBuilder hexString = new StringBuilder(Integer.toHexString(b & 255)); - while (hexString.length() < 2) { - hexString.insert(0, "0"); - } - sb.append(hexString); + for (var b : md5) { + var hex = Integer.toHexString(b & 0xFF); + if (hex.length() == 1) + sb.append(0); + sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); - return null; } + return null; } } \ No newline at end of file diff --git a/smali/smali_classes4/com/vtosters/lite/audio/player/ExoPlayerHelper.smali b/smali/smali_classes4/com/vtosters/lite/audio/player/ExoPlayerHelper.smali index 68cb035b1..c5605a9dd 100644 --- a/smali/smali_classes4/com/vtosters/lite/audio/player/ExoPlayerHelper.smali +++ b/smali/smali_classes4/com/vtosters/lite/audio/player/ExoPlayerHelper.smali @@ -703,7 +703,7 @@ .line 17 :cond_0 - invoke-static {p1}, Lru/vtosters/lite/music/Scrobbler;->grabMusicTrack(Lcom/vk/dto/music/MusicTrack;)V + invoke-static {p1}, Lru/vtosters/lite/music/LastFMScrobbler;->grabMusicTrack(Lcom/vk/dto/music/MusicTrack;)V invoke-virtual {p1}, Lcom/vk/dto/music/MusicTrack;->y1()Ljava/lang/String;