0.6.14 - Fix FLAC for HiFi users

This commit is contained in:
exttex 2021-07-25 15:05:48 +02:00
parent 97cc8c92ff
commit 9d0c4c521d
4 changed files with 235 additions and 114 deletions

View File

@ -1,6 +1,7 @@
package f.f.freezer; package f.f.freezer;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO; import org.jaudiotagger.audio.AudioFileIO;
@ -36,6 +37,7 @@ public class Deezer {
String token; String token;
String arl; String arl;
String sid; String sid;
String accessToken;
boolean authorized = false; boolean authorized = false;
boolean authorizing = false; boolean authorizing = false;
@ -63,9 +65,98 @@ public class Deezer {
logger.warn("Error authorizing to Deezer API! " + e.toString()); logger.warn("Error authorizing to Deezer API! " + e.toString());
} }
} }
//TV API
if (accessToken == null) {
try {
authorizeTVAPI();
} catch (Exception e) {
this.accessToken = null;
e.printStackTrace();
logger.warn("Error authorizing TV API - FLAC might not be available! " + e.toString());
}
}
authorizing = false; authorizing = false;
} }
//Make POST request
private String POST(String _url, String data, String cookie) throws Exception {
URL url = new URL(_url);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setConnectTimeout(20000);
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
connection.setRequestProperty("Accept-Language", "*");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "*/*");
connection.setRequestProperty("Content-Length", Integer.toString(data.getBytes(StandardCharsets.UTF_8).length));
if (cookie != null) {
connection.setRequestProperty("Cookie", cookie);
}
//Write body
DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
wr.writeBytes(data);
wr.close();
//Get response
String output = "";
Scanner scanner = new Scanner(connection.getInputStream());
while (scanner.hasNext()) {
output += scanner.nextLine();
}
scanner.close();
connection.disconnect();
return output;
}
private String GET(String _url) throws Exception {
URL url = new URL(_url);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setConnectTimeout(20000);
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
connection.setRequestProperty("Accept-Language", "*");
connection.setRequestProperty("Accept", "*/*");
//Get response
String output = "";
Scanner scanner = new Scanner(connection.getInputStream());
while (scanner.hasNext()) {
output += scanner.nextLine();
}
scanner.close();
connection.disconnect();
return output;
}
//Authorize TV API for generating URLs
public void authorizeTVAPI() throws Exception {
JSONObject json = new JSONObject(POST(
"https://distribution-api.deezer.com/device/token",
"{\"brand_name\":\"Hisense\",\"device_id\":\"7239e4071d8992c955ad\",\"model_name\":\"HE50A6109FUWTS\",\"country_code\":\"FRA\"}",
null
));
String deviceToken = json.getString("device_token");
// Get unauthorized token
json = new JSONObject(GET("https://connect.deezer.com/oauth/access_token.php?grant_type=client_credentials&client_id=447462&client_secret=a83bf7f38ad2f137e444727cfc3775cf&output=json"));
String accessToken = json.getString("access_token");
// Get smart login code
json = new JSONObject(POST(
"https://connect.deezer.com/2.0/smartlogin/device?access_token=" + accessToken + "&device_token=" + deviceToken,
"", null
));
String smartLoginCode = json.getJSONObject("data").getString("smartLoginCode");
// Get the fuck that is
callGWAPI("deezer.associateSmartLoginCodeWithUser", "{\"SMARTLOGIN_CODE\": \"" + smartLoginCode + "\"}");
// Get authorized access tonk
json = new JSONObject(GET("https://connect.deezer.com/2.0/smartlogin/" + smartLoginCode + "?access_token=" + accessToken));
accessToken = json.getJSONObject("data").getString("accessToken");
this.accessToken = accessToken;
Log.d("DDDD", "Authorized TV: " + this.accessToken);
}
public JSONObject callGWAPI(String method, String params) throws Exception { public JSONObject callGWAPI(String method, String params) throws Exception {
//Get token //Get token
if (token == null) { if (token == null) {
@ -73,36 +164,11 @@ public class Deezer {
callGWAPI("deezer.getUserData", "{}"); callGWAPI("deezer.getUserData", "{}");
} }
//Call String data = POST(
URL url = new URL("https://www.deezer.com/ajax/gw-light.php?method=" + method + "&input=3&api_version=1.0&api_token=" + token); "https://www.deezer.com/ajax/gw-light.php?method=" + method + "&input=3&api_version=1.0&api_token=" + token,
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); params,
connection.setConnectTimeout(20000); "arl=" + arl + "; sid=" + sid
connection.setDoOutput(true); );
connection.setRequestMethod("POST");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
connection.setRequestProperty("Accept-Language", "*");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "*/*");
connection.setRequestProperty("Content-Length", Integer.toString(params.getBytes(StandardCharsets.UTF_8).length));
String cookies = "arl=" + arl + "; sid=" + sid;
connection.setRequestProperty("Cookie", cookies);
//Write body
DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
wr.writeBytes(params);
wr.close();
//Get response
String data = "";
Scanner scanner = new Scanner(connection.getInputStream());
while (scanner.hasNext()) {
data += scanner.nextLine();
}
//End
try {
connection.disconnect();
scanner.close();
} catch (Exception e) {}
//Parse JSON //Parse JSON
JSONObject out = new JSONObject(data); JSONObject out = new JSONObject(data);
@ -110,16 +176,7 @@ public class Deezer {
//Save token //Save token
if ((token == null || token.equals("null")) && method.equals("deezer.getUserData")) { if ((token == null || token.equals("null")) && method.equals("deezer.getUserData")) {
token = out.getJSONObject("results").getString("checkForm"); token = out.getJSONObject("results").getString("checkForm");
//SID sid = out.getJSONObject("results").getString("SESSION_ID");
try {
String newSid = null;
for (String cookie : connection.getHeaderFields().get("Set-Cookie")) {
if (cookie.startsWith("sid=")) {
newSid = cookie.split(";")[0].split("=")[1];
}
}
this.sid = newSid;
} catch (Exception ignored) {}
} }
return out; return out;
@ -153,7 +210,7 @@ public class Deezer {
} }
//Generate track download URL //Generate track download URL
public static String getTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) { public static String generateTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
try { try {
int magic = 164; int magic = 164;
@ -201,6 +258,39 @@ public class Deezer {
return null; return null;
} }
// Returns URL and wether encrypted
public Pair<String, Boolean> getTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
// TV API URL Gen
if (this.accessToken != null && quality == 9) {
try {
URL url = new URL("https://api.deezer.com/platform/gcast/track/" + trackId + "/streamUrls");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setConnectTimeout(20000);
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
connection.setRequestProperty("Accept-Language", "*");
connection.setRequestProperty("Accept", "*/*");
connection.setRequestProperty("Authorization", "Bearer " + this.accessToken);
//Get response
String output = "";
Scanner scanner = new Scanner(connection.getInputStream());
while (scanner.hasNext()) {
output += scanner.nextLine();
}
scanner.close();
connection.disconnect();
JSONObject json = new JSONObject(output).getJSONObject("data").getJSONObject("attributes");
return new Pair<String, Boolean>(json.getString("url_flac"), false);
} catch (Exception e) {
e.printStackTrace();
logger.warn("Failed generating ATV URL!");
}
}
// Normal url gen
return new Pair<String, Boolean>(Deezer.generateTrackUrl(trackId, md5origin, mediaVersion, quality), true);
}
public static String bytesToHex(byte[] bytes) { public static String bytesToHex(byte[] bytes) {
final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2]; char[] hexChars = new char[bytes.length * 2];
@ -499,6 +589,7 @@ public class Deezer {
String trackId; String trackId;
int initialQuality; int initialQuality;
DownloadLog logger; DownloadLog logger;
boolean encrypted;
QualityInfo(int quality, String trackId, String md5origin, String mediaVersion, DownloadLog logger) { QualityInfo(int quality, String trackId, String md5origin, String mediaVersion, DownloadLog logger) {
this.quality = quality; this.quality = quality;
@ -509,16 +600,16 @@ public class Deezer {
this.logger = logger; this.logger = logger;
} }
boolean fallback(Deezer deezer) { String fallback(Deezer deezer) {
//Quality fallback //Quality fallback
try { try {
qualityFallback(); String url = qualityFallback(deezer);
//No quality //No quality
if (quality == -1) if (quality == -1)
throw new Exception("No quality to fallback to!"); throw new Exception("No quality to fallback to!");
//Success //Success
return true; return url;
} catch (Exception e) { } catch (Exception e) {
logger.warn("Quality fallback failed! ID: " + trackId + " " + e.toString()); logger.warn("Quality fallback failed! ID: " + trackId + " " + e.toString());
quality = initialQuality; quality = initialQuality;
@ -562,12 +653,15 @@ public class Deezer {
logger.error("ISRC Fallback failed, track unavailable! ID: " + trackId + " " + e.toString()); logger.error("ISRC Fallback failed, track unavailable! ID: " + trackId + " " + e.toString());
} }
return false; return null;
} }
private void qualityFallback() throws Exception { private String qualityFallback(Deezer deezer) throws Exception {
Pair<String,Boolean> urlGen = deezer.getTrackUrl(trackId, md5origin, mediaVersion, quality);
this.encrypted = urlGen.second;
//Create HEAD requests to check if exists //Create HEAD requests to check if exists
URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, quality)); URL url = new URL(urlGen.first);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("HEAD"); connection.setRequestMethod("HEAD");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"); connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
@ -580,12 +674,13 @@ public class Deezer {
//-1 if no quality available //-1 if no quality available
if (quality == 1) { if (quality == 1) {
quality = -1; quality = -1;
return; return null;
} }
if (quality == 3) quality = 1; if (quality == 3) quality = 1;
if (quality == 9) quality = 3; if (quality == 9) quality = 3;
qualityFallback(); return qualityFallback(deezer);
} }
return urlGen.first;
} }
} }

View File

@ -17,6 +17,7 @@ import android.os.Message;
import android.os.Messenger; import android.os.Messenger;
import android.os.RemoteException; import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
@ -318,10 +319,11 @@ public class DownloadService extends Service {
//Fallback //Fallback
Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(this.download.quality, this.download.trackId, this.download.md5origin, this.download.mediaVersion, logger); Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(this.download.quality, this.download.trackId, this.download.md5origin, this.download.mediaVersion, logger);
String sURL = null;
if (!download.isUserUploaded()) { if (!download.isUserUploaded()) {
try { try {
boolean res = qualityInfo.fallback(deezer); sURL = qualityInfo.fallback(deezer);
if (!res) if (sURL == null)
throw new Exception("No more to fallback!"); throw new Exception("No more to fallback!");
download.quality = qualityInfo.quality; download.quality = qualityInfo.quality;
@ -378,7 +380,6 @@ public class DownloadService extends Service {
} }
//Download //Download
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
try { try {
URL url = new URL(sURL); URL url = new URL(sURL);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
@ -436,15 +437,17 @@ public class DownloadService extends Service {
//Post processing //Post processing
//Decrypt //Decrypt
try { if (qualityInfo.encrypted) {
File decFile = new File(tmpFile.getPath() + ".DEC"); try {
deezer.decryptFile(download.trackId, tmpFile.getPath(), decFile.getPath()); File decFile = new File(tmpFile.getPath() + ".DEC");
tmpFile.delete(); deezer.decryptFile(download.trackId, tmpFile.getPath(), decFile.getPath());
tmpFile = decFile; tmpFile.delete();
} catch (Exception e) { tmpFile = decFile;
logger.error("Decryption error: " + e.toString(), download); } catch (Exception e) {
e.printStackTrace(); logger.error("Decryption error: " + e.toString(), download);
//Shouldn't ever fail e.printStackTrace();
//Shouldn't ever fail
}
} }

View File

@ -28,6 +28,7 @@ public class StreamServer {
//Shared log & API //Shared log & API
private DownloadLog logger; private DownloadLog logger;
private Deezer deezer; private Deezer deezer;
private boolean authorized = false;
StreamServer(String arl, String offlinePath) { StreamServer(String arl, String offlinePath) {
//Initialize shared variables //Initialize shared variables
@ -174,6 +175,12 @@ public class StreamServer {
} }
private Response deezerStream(IHTTPSession session, int startBytes, int end, boolean isRanged) { private Response deezerStream(IHTTPSession session, int startBytes, int end, boolean isRanged) {
// Authorize
if (!authorized) {
deezer.authorize();
authorized = true;
}
//Get QP into Quality Info //Get QP into Quality Info
Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo( Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(
Integer.parseInt(session.getParameters().get("q").get(0)), Integer.parseInt(session.getParameters().get("q").get(0)),
@ -183,19 +190,23 @@ public class StreamServer {
logger logger
); );
//Fallback //Fallback
String sURL;
try { try {
boolean res = qualityInfo.fallback(deezer); sURL = qualityInfo.fallback(deezer);
if (!res) if (sURL == null)
throw new Exception("No more to fallback!"); throw new Exception("No more to fallback!");
} catch (Exception e) { } catch (Exception e) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Fallback failed!"); return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Fallback failed!");
} }
//Calculate Deezer offsets //Calculate Deezer offsets
int deezerStart = startBytes - (startBytes % 2048); int _deezerStart = startBytes;
if (qualityInfo.encrypted)
_deezerStart -= startBytes % 2048;
final int deezerStart = _deezerStart;
int dropBytes = startBytes % 2048; int dropBytes = startBytes % 2048;
//Start download //Start download
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
try { try {
URL url = new URL(sURL); URL url = new URL(sURL);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
@ -208,58 +219,70 @@ public class StreamServer {
connection.setRequestProperty("Range", "bytes=" + Integer.toString(deezerStart) + "-" + ((end == -1) ? "" : Integer.toString(end))); connection.setRequestProperty("Range", "bytes=" + Integer.toString(deezerStart) + "-" + ((end == -1) ? "" : Integer.toString(end)));
connection.connect(); connection.connect();
//Get decryption key Response outResponse;
final byte[] key = Deezer.getKey(qualityInfo.trackId); // Encrypted response
if (qualityInfo.encrypted) {
//Get decryption key
final byte[] key = Deezer.getKey(qualityInfo.trackId);
//Write response headers outResponse = newFixedLengthResponse(
Response outResponse = newFixedLengthResponse( isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK,
(qualityInfo.quality == 9) ? "audio/flac" : "audio/mpeg",
new BufferedInputStream(new FilterInputStream(connection.getInputStream()) {
int counter = deezerStart / 2048;
int drop = dropBytes;
//Decryption stream
@Override
public int read(byte[] b, int off, int len) throws IOException {
//Read 2048b or EOF
byte[] buffer = new byte[2048];
int read = 0;
int totalRead = 0;
while (read != -1 && totalRead != 2048) {
read = in.read(buffer, totalRead, 2048 - totalRead);
if (read != -1)
totalRead += read;
}
if (totalRead == 0)
return -1;
//Not full chunk return unencrypted
if (totalRead != 2048) {
System.arraycopy(buffer, 0, b, off, totalRead);
return totalRead;
}
//Decrypt
if ((counter % 3) == 0) {
buffer = Deezer.decryptChunk(key, buffer);
}
//Drop bytes from rounding to 2048
if (drop > 0) {
int output = 2048 - drop;
System.arraycopy(buffer, drop, b, off, output);
drop = 0;
counter++;
return output;
}
//Copy
System.arraycopy(buffer, 0, b, off, 2048);
counter++;
return 2048;
}
}, 2048),
connection.getContentLength() - dropBytes
);
} else {
// Decrypted
outResponse = newFixedLengthResponse(
isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK, isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK,
(qualityInfo.quality == 9) ? "audio/flac" : "audio/mpeg", (qualityInfo.quality == 9) ? "audio/flac" : "audio/mpeg",
new BufferedInputStream(new FilterInputStream(connection.getInputStream()) { connection.getInputStream(),
connection.getContentLength()
);
}
int counter = deezerStart / 2048;
int drop = dropBytes;
//Decryption stream
@Override
public int read(byte[] b, int off, int len) throws IOException {
//Read 2048b or EOF
byte[] buffer = new byte[2048];
int read = 0;
int totalRead = 0;
while (read != -1 && totalRead != 2048) {
read = in.read(buffer, totalRead, 2048 - totalRead);
if (read != -1)
totalRead += read;
}
if (totalRead == 0)
return -1;
//Not full chunk return unencrypted
if (totalRead != 2048) {
System.arraycopy(buffer, 0, b, off, totalRead);
return totalRead;
}
//Decrypt
if ((counter % 3) == 0) {
buffer = Deezer.decryptChunk(key, buffer);
}
//Drop bytes from rounding to 2048
if (drop > 0) {
int output = 2048 - drop;
System.arraycopy(buffer, drop, b, off, output);
drop = 0;
counter++;
return output;
}
//Copy
System.arraycopy(buffer, 0, b, off, 2048);
counter++;
return 2048;
}
}, 2048),
connection.getContentLength() - dropBytes
);
//Ranged header //Ranged header
if (isRanged) { if (isRanged) {
String range = "bytes " + Integer.toString(startBytes) + "-" + Integer.toString((end == -1) ? (connection.getContentLength() + deezerStart) - 1 : end); String range = "bytes " + Integer.toString(startBytes) + "-" + Integer.toString((end == -1) ? (connection.getContentLength() + deezerStart) - 1 : end);

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.13+1 version: 0.6.14+1
environment: environment:
sdk: ">=2.8.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"