Skip to content

Commit

Permalink
Implement new lyrics API in download back-end
Browse files Browse the repository at this point in the history
  • Loading branch information
DJDoubleD committed Jul 24, 2024
1 parent 318f980 commit 3a29640
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 29 deletions.
147 changes: 125 additions & 22 deletions android/app/src/main/java/r/r/refreezer/Deezer.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@
import java.net.URL;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;

import r.r.refreezer.models.Lyrics;
import r.r.refreezer.models.LyricsNew;
import r.r.refreezer.models.SynchronizedLyric;

public class Deezer {

static String USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36";
Expand Down Expand Up @@ -74,7 +80,7 @@ public void authorize() {
}

//Make POST request
private String POST(String _url, String data, String cookies) {
private String POST(String _url, String data, Map<String, String> additionalHeaders) {
String result = null;

try {
Expand All @@ -87,13 +93,19 @@ private String POST(String _url, String data, String cookies) {
connection.setRequestProperty("Accept-Language", contentLanguage + ",*");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "*/*");
if (cookies != null) {
connection.setRequestProperty("Cookie", cookies);

// Add additional headers if provided
if (additionalHeaders != null && !additionalHeaders.isEmpty()) {
for (Map.Entry<String, String> entry : additionalHeaders.entrySet()) {
connection.setRequestProperty(entry.getKey(), entry.getValue());
}
}

//Write body
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.writeBytes(data);
if (data != null) {
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.writeBytes(data);
}
}

//Get response
Expand All @@ -118,10 +130,14 @@ public JSONObject callGWAPI(String method, String body) throws Exception {
callGWAPI("deezer.getUserData", "{}");
}

// Construct cookie header
Map<String, String> cookies = new HashMap<>();
cookies.put("Cookie", "arl=" + arl + (sid == null ? "" : "; sid=" + sid));

String data = POST(
"https://www.deezer.com/ajax/gw-light.php?method=" + method + "&input=3&api_version=1.0&api_token=" + token,
body,
"arl=" + arl + "; sid=" + sid
cookies
);

//Parse JSON
Expand All @@ -145,7 +161,6 @@ public JSONObject callGWAPI(String method, String body) throws Exception {
return out;
}


//api.deezer.com/$method/$param
public JSONObject callPublicAPI(String method, String param) throws Exception {
URL url = new URL("https://api.deezer.com/" + method + "/" + param);
Expand All @@ -170,6 +185,91 @@ public JSONObject callPublicAPI(String method, String param) throws Exception {
return new JSONObject(data.toString());
}

// Method to call the Pipe API
public JSONObject callPipeApi(Map<String, Object> params) throws Exception {
String jwtToken = "";
try {
jwtToken = getJsonWebToken();
} catch (Exception e) {
logger.error("Error getting JsonWebToken: " + e);
throw e;
}

// Headers
Map<String, String> headers = new HashMap<>();
headers.put("Cookie", "arl=" + arl + (sid == null ? "" : "; sid=" + sid));
headers.put("Authorization", "Bearer " + jwtToken);

// Convert params to JSON string
String paramsJsonString = null;
if (params != null && !params.isEmpty()) {
JSONObject paramsJsonObject = new JSONObject(params);
paramsJsonString = paramsJsonObject.toString();
}

String response = POST("https://pipe.deezer.com/api/", paramsJsonString, headers);
// Return response as JSONObject
return new JSONObject(response);
}

// Method to get JSON Web Token
public String getJsonWebToken() throws Exception{
String urlString = "https://auth.deezer.com/login/arl?jo=p&rto=c&i=c";
Map<String, String> cookies = new HashMap<>();
cookies.put("Cookie", "arl=" + arl + (sid == null ? "" : "; sid=" + sid));
String response = POST(urlString, "", cookies);

// Parse JSON and return JWT
JSONObject body = null;
body = new JSONObject(response);
return body.has("jwt") ? body.getString("jwt") : "";
}

public Lyrics getlyricsNew(String trackId) {
try {
// Create the GraphQL query string
String queryStringGraphQL =
"query SynchronizedTrackLyrics($trackId: String!) {" +
" track(trackId: $trackId) {" +
" id" +
" isExplicit" +
" lyrics {" +
" id" +
" copyright" +
" text" +
" writers" +
" synchronizedLines {" +
" lrcTimestamp" +
" line" +
" milliseconds" +
" duration" +
" }" +
" }" +
" }" +
"}";

// Create the request parameters
Map<String, Object> requestParams = new HashMap<>();
requestParams.put("operationName", "SynchronizedTrackLyrics");
Map<String, String> variables = new HashMap<>();
variables.put("trackId", trackId);
requestParams.put("variables", variables);
requestParams.put("query", queryStringGraphQL);

// Call the API
JSONObject data = callPipeApi(requestParams);

// Parse the response into a LyricsFull object
return new LyricsNew(data);

} catch (Exception e) {
e.printStackTrace();
Lyrics errorLyrics = new LyricsNew();
errorLyrics.setErrorMessage("An error occurred: " + e.getMessage());
return errorLyrics;
}
}

//Generate track download URL
public String generateTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
try {
Expand Down Expand Up @@ -230,13 +330,16 @@ public Pair<String, Boolean> getTrackUrl(String trackId, String trackToken, Stri
if (quality == 3) format = "MP3_320";

try {
//arl cookie
Map<String, String> cookies = new HashMap<>();
cookies.put("Cookie", "arl=" + arl);
// Create track_url payload
String payload = "{\n" +
"\"license_token\": \"" + licenseToken + "\",\n" +
"\"media\": [{ \"type\": \"FULL\", \"formats\": [{ \"cipher\": \"BF_CBC_STRIPE\", \"format\": \"" + format + "\"}]}],\n" +
"\"track_tokens\": [\"" + trackToken + "\"]\n" +
"}";
String output = POST("https://media.deezer.com/v1/get_url", payload, "arl=" + arl);
String output = POST("https://media.deezer.com/v1/get_url", payload, cookies);

JSONObject result = new JSONObject(output);

Expand Down Expand Up @@ -333,7 +436,7 @@ public static String generateUserUploadedMP3Filename(String original, String tit
}

//Tag track with data from API
public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson, DownloadService.DownloadSettings settings) throws Exception {
public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, Lyrics lyricsData, JSONObject privateJson, DownloadService.DownloadSettings settings) throws Exception {
TagOptionSingleton.getInstance().setAndroid(true);
//Load file
AudioFile f = AudioFileIO.read(new File(path));
Expand Down Expand Up @@ -369,10 +472,9 @@ public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum
if (settings.tags.bpm) tag.setField(FieldKey.BPM, Integer.toString((int)publicTrack.getDouble("bpm")));

//Unsynced lyrics
if (lyricsData != null && settings.tags.lyrics) {
if (settings.tags.lyrics && lyricsData != null && lyricsData.getUnsyncedLyrics() != null && !lyricsData.getUnsyncedLyrics().isEmpty()) {
try {
String lyrics = lyricsData.getString("LYRICS_TEXT");
tag.setField(FieldKey.LYRICS, lyrics);
tag.setField(FieldKey.LYRICS, lyricsData.getUnsyncedLyrics());
} catch (Exception e) {
Log.w("WARN", "Error adding unsynced lyrics!");
}
Expand Down Expand Up @@ -497,8 +599,8 @@ public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum
}

//Create JSON file, privateJsonData = `song.getLyrics`
public static String generateLRC(JSONObject privateJsonData, JSONObject publicTrack) throws Exception {
String output = "";
public static String generateLRC(Lyrics lyricsData, JSONObject publicTrack) throws Exception {
StringBuilder output = new StringBuilder();

//Create metadata
String title = publicTrack.getString("title");
Expand All @@ -508,21 +610,22 @@ public static String generateLRC(JSONObject privateJsonData, JSONObject publicTr
artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
}
//Write metadata
output += "[ar:" + artists.substring(2) + "]\r\n[al:" + album + "]\r\n[ti:" + title + "]\r\n";
output.append("[ar:").append(artists.substring(2)).append("]\r\n[al:").append(album).append("]\r\n[ti:").append(title).append("]\r\n");

//Get lyrics
int counter = 0;
JSONArray syncLyrics = privateJsonData.getJSONArray("LYRICS_SYNC_JSON");
for (int i=0; i<syncLyrics.length(); i++) {
JSONObject lyric = syncLyrics.getJSONObject(i);
if (lyric.has("lrc_timestamp") && lyric.has("line")) {
output += lyric.getString("lrc_timestamp") + lyric.getString("line") + "\r\n";
counter += 1;
if (lyricsData.getSyncedLyrics() != null){
for (int i=0; i<lyricsData.getSyncedLyrics().size(); i++) {
SynchronizedLyric lyric = lyricsData.getSyncedLyrics().get(i);
if (lyric.getLrcTimestamp() != null && lyric.getText() != null) {
output.append(lyric.getLrcTimestamp()).append(lyric.getText()).append("\r\n");
counter += 1;
}
}
}

if (counter == 0) throw new Exception("Empty Lyrics!");
return output;
return output.toString();
}

static class QualityInfo {
Expand Down
29 changes: 22 additions & 7 deletions android/app/src/main/java/r/r/refreezer/DownloadService.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@

import javax.net.ssl.HttpsURLConnection;

import r.r.refreezer.models.Lyrics;
import r.r.refreezer.models.LyricsClassic;

public class DownloadService extends Service {

//Message commands
Expand Down Expand Up @@ -280,7 +283,7 @@ public class DownloadThread extends Thread {
JSONObject trackJson;
JSONObject albumJson;
JSONObject privateJson;
JSONObject lyricsData = null;
Lyrics lyricsData = null;
boolean stopDownload = false;

DownloadThread(Download download) {
Expand All @@ -305,16 +308,28 @@ public void run() {
//Don't fetch meta if user uploaded mp3
if (!download.isUserUploaded()) {
try {
JSONObject privateRaw = deezer.callGWAPI("deezer.pageTrack", "{\"sng_id\": \"" + download.trackId + "\"}");
privateJson = privateRaw.getJSONObject("results").getJSONObject("DATA");
if (privateRaw.getJSONObject("results").has("LYRICS")) {
lyricsData = privateRaw.getJSONObject("results").getJSONObject("LYRICS");
}
trackJson = deezer.callPublicAPI("track", download.trackId);
albumJson = deezer.callPublicAPI("album", Integer.toString(trackJson.getJSONObject("album").getInt("id")));

try {
lyricsData = deezer.getlyricsNew(download.trackId);

if (lyricsData.getErrorMessage() != null || !lyricsData.isLoaded()) {
logger.error("Unable the get lyrics from Pipe API: " + lyricsData.getErrorMessage());
logger.warn("Using classic API for lyrics");

JSONObject privateRaw = deezer.callGWAPI("deezer.pageTrack", "{\"sng_id\": \"" + download.trackId + "\"}");
privateJson = privateRaw.getJSONObject("results").getJSONObject("DATA");
if (privateRaw.getJSONObject("results").has("LYRICS")) {
lyricsData = new LyricsClassic(privateRaw.getJSONObject("results").getJSONObject("LYRICS"));
}
}
} catch (Exception e) {
logger.error("Unable to fetch lyrics data! " + e, download);
e.printStackTrace();
}
} catch (Exception e) {
logger.error("Unable to fetch track and album metadata! " + e.toString(), download);
logger.error("Unable to fetch track and album metadata! " + e, download);
e.printStackTrace();
download.state = Download.DownloadState.ERROR;
exit();
Expand Down
86 changes: 86 additions & 0 deletions android/app/src/main/java/r/r/refreezer/models/Lyrics.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package r.r.refreezer.models;

import java.util.ArrayList;
import java.util.List;

public abstract class Lyrics {
protected String id;
protected String writers;
protected List<SynchronizedLyric> syncedLyrics;
protected String errorMessage;
protected String unsyncedLyrics;
protected Boolean isExplicit;
protected String copyright;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getWriters() {
return writers;
}

public void setWriters(String writers) {
this.writers = writers;
}

public List<SynchronizedLyric> getSyncedLyrics() {
return syncedLyrics;
}

public void setSyncedLyrics(List<SynchronizedLyric> syncedLyrics) {
this.syncedLyrics = syncedLyrics;
}

public String getErrorMessage() {
return errorMessage;
}

public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}

public String getUnsyncedLyrics() {
return unsyncedLyrics;
}

public void setUnsyncedLyrics(String unsyncedLyrics) {
this.unsyncedLyrics = unsyncedLyrics;
}

public Boolean getExplicit() {
return isExplicit;
}

public void setExplicit(Boolean explicit) {
isExplicit = explicit;
}

public String getCopyright() {
return copyright;
}

public void setCopyright(String copyright) {
this.copyright = copyright;
}

public Lyrics() {
this.syncedLyrics = new ArrayList<>();
}

public boolean isLoaded() {
return (syncedLyrics != null && !syncedLyrics.isEmpty()) || (unsyncedLyrics != null && !unsyncedLyrics.isEmpty());
}

public boolean isSynced() {
return syncedLyrics != null && syncedLyrics.size() > 1;
}

public boolean isUnsynced() {
return !isSynced() && (unsyncedLyrics != null && !unsyncedLyrics.isEmpty());
}
}
Loading

0 comments on commit 3a29640

Please sign in to comment.