refactored last.fm scrobbler

This commit is contained in:
Vologhat
2022-10-08 10:37:47 +03:00
parent bc25be1a27
commit 19fb86ca58
5 changed files with 242 additions and 238 deletions
@@ -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<String> 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<String, String>();
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<String, String>();
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<String, String> 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<String, String> 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);
}
}
@@ -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<String> 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<String, String> 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<String, String> map) {
var sb = new StringBuilder();
for (Map.Entry<String, String> 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<String, String> 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);
}
}
@@ -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();
@@ -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;
}
}
@@ -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;