0.6.14 - Fix FLAC for HiFi users
This commit is contained in:
parent
97cc8c92ff
commit
9d0c4c521d
|
@ -1,6 +1,7 @@
|
|||
package f.f.freezer;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFile;
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
|
@ -36,6 +37,7 @@ public class Deezer {
|
|||
String token;
|
||||
String arl;
|
||||
String sid;
|
||||
String accessToken;
|
||||
boolean authorized = false;
|
||||
boolean authorizing = false;
|
||||
|
||||
|
@ -63,9 +65,98 @@ public class Deezer {
|
|||
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;
|
||||
}
|
||||
|
||||
//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 {
|
||||
//Get token
|
||||
if (token == null) {
|
||||
|
@ -73,36 +164,11 @@ public class Deezer {
|
|||
callGWAPI("deezer.getUserData", "{}");
|
||||
}
|
||||
|
||||
//Call
|
||||
URL url = new URL("https://www.deezer.com/ajax/gw-light.php?method=" + method + "&input=3&api_version=1.0&api_token=" + token);
|
||||
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(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) {}
|
||||
String data = POST(
|
||||
"https://www.deezer.com/ajax/gw-light.php?method=" + method + "&input=3&api_version=1.0&api_token=" + token,
|
||||
params,
|
||||
"arl=" + arl + "; sid=" + sid
|
||||
);
|
||||
|
||||
//Parse JSON
|
||||
JSONObject out = new JSONObject(data);
|
||||
|
@ -110,16 +176,7 @@ public class Deezer {
|
|||
//Save token
|
||||
if ((token == null || token.equals("null")) && method.equals("deezer.getUserData")) {
|
||||
token = out.getJSONObject("results").getString("checkForm");
|
||||
//SID
|
||||
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) {}
|
||||
sid = out.getJSONObject("results").getString("SESSION_ID");
|
||||
}
|
||||
|
||||
return out;
|
||||
|
@ -153,7 +210,7 @@ public class Deezer {
|
|||
}
|
||||
|
||||
//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 {
|
||||
int magic = 164;
|
||||
|
||||
|
@ -201,6 +258,39 @@ public class Deezer {
|
|||
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) {
|
||||
final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
|
@ -499,6 +589,7 @@ public class Deezer {
|
|||
String trackId;
|
||||
int initialQuality;
|
||||
DownloadLog logger;
|
||||
boolean encrypted;
|
||||
|
||||
QualityInfo(int quality, String trackId, String md5origin, String mediaVersion, DownloadLog logger) {
|
||||
this.quality = quality;
|
||||
|
@ -509,16 +600,16 @@ public class Deezer {
|
|||
this.logger = logger;
|
||||
}
|
||||
|
||||
boolean fallback(Deezer deezer) {
|
||||
String fallback(Deezer deezer) {
|
||||
//Quality fallback
|
||||
try {
|
||||
qualityFallback();
|
||||
String url = qualityFallback(deezer);
|
||||
//No quality
|
||||
if (quality == -1)
|
||||
throw new Exception("No quality to fallback to!");
|
||||
|
||||
//Success
|
||||
return true;
|
||||
return url;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Quality fallback failed! ID: " + trackId + " " + e.toString());
|
||||
quality = initialQuality;
|
||||
|
@ -562,12 +653,15 @@ public class Deezer {
|
|||
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
|
||||
URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, quality));
|
||||
URL url = new URL(urlGen.first);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||
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");
|
||||
|
@ -580,12 +674,13 @@ public class Deezer {
|
|||
//-1 if no quality available
|
||||
if (quality == 1) {
|
||||
quality = -1;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (quality == 3) quality = 1;
|
||||
if (quality == 9) quality = 3;
|
||||
qualityFallback();
|
||||
return qualityFallback(deezer);
|
||||
}
|
||||
return urlGen.first;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import android.os.Message;
|
|||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
@ -318,10 +319,11 @@ public class DownloadService extends Service {
|
|||
|
||||
//Fallback
|
||||
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()) {
|
||||
try {
|
||||
boolean res = qualityInfo.fallback(deezer);
|
||||
if (!res)
|
||||
sURL = qualityInfo.fallback(deezer);
|
||||
if (sURL == null)
|
||||
throw new Exception("No more to fallback!");
|
||||
|
||||
download.quality = qualityInfo.quality;
|
||||
|
@ -378,7 +380,6 @@ public class DownloadService extends Service {
|
|||
}
|
||||
|
||||
//Download
|
||||
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
|
||||
try {
|
||||
URL url = new URL(sURL);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||
|
@ -436,15 +437,17 @@ public class DownloadService extends Service {
|
|||
//Post processing
|
||||
|
||||
//Decrypt
|
||||
try {
|
||||
File decFile = new File(tmpFile.getPath() + ".DEC");
|
||||
deezer.decryptFile(download.trackId, tmpFile.getPath(), decFile.getPath());
|
||||
tmpFile.delete();
|
||||
tmpFile = decFile;
|
||||
} catch (Exception e) {
|
||||
logger.error("Decryption error: " + e.toString(), download);
|
||||
e.printStackTrace();
|
||||
//Shouldn't ever fail
|
||||
if (qualityInfo.encrypted) {
|
||||
try {
|
||||
File decFile = new File(tmpFile.getPath() + ".DEC");
|
||||
deezer.decryptFile(download.trackId, tmpFile.getPath(), decFile.getPath());
|
||||
tmpFile.delete();
|
||||
tmpFile = decFile;
|
||||
} catch (Exception e) {
|
||||
logger.error("Decryption error: " + e.toString(), download);
|
||||
e.printStackTrace();
|
||||
//Shouldn't ever fail
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ public class StreamServer {
|
|||
//Shared log & API
|
||||
private DownloadLog logger;
|
||||
private Deezer deezer;
|
||||
private boolean authorized = false;
|
||||
|
||||
StreamServer(String arl, String offlinePath) {
|
||||
//Initialize shared variables
|
||||
|
@ -174,6 +175,12 @@ public class StreamServer {
|
|||
}
|
||||
|
||||
private Response deezerStream(IHTTPSession session, int startBytes, int end, boolean isRanged) {
|
||||
// Authorize
|
||||
if (!authorized) {
|
||||
deezer.authorize();
|
||||
authorized = true;
|
||||
}
|
||||
|
||||
//Get QP into Quality Info
|
||||
Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(
|
||||
Integer.parseInt(session.getParameters().get("q").get(0)),
|
||||
|
@ -183,19 +190,23 @@ public class StreamServer {
|
|||
logger
|
||||
);
|
||||
//Fallback
|
||||
String sURL;
|
||||
try {
|
||||
boolean res = qualityInfo.fallback(deezer);
|
||||
if (!res)
|
||||
sURL = qualityInfo.fallback(deezer);
|
||||
if (sURL == null)
|
||||
throw new Exception("No more to fallback!");
|
||||
} catch (Exception e) {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Fallback failed!");
|
||||
}
|
||||
|
||||
//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;
|
||||
|
||||
//Start download
|
||||
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
|
||||
try {
|
||||
URL url = new URL(sURL);
|
||||
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.connect();
|
||||
|
||||
//Get decryption key
|
||||
final byte[] key = Deezer.getKey(qualityInfo.trackId);
|
||||
Response outResponse;
|
||||
// Encrypted response
|
||||
if (qualityInfo.encrypted) {
|
||||
//Get decryption key
|
||||
final byte[] key = Deezer.getKey(qualityInfo.trackId);
|
||||
|
||||
//Write response headers
|
||||
Response outResponse = newFixedLengthResponse(
|
||||
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,
|
||||
(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
|
||||
if (isRanged) {
|
||||
String range = "bytes " + Integer.toString(startBytes) + "-" + Integer.toString((end == -1) ? (connection.getContentLength() + deezerStart) - 1 : end);
|
||||
|
|
|
@ -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.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.6.13+1
|
||||
version: 0.6.14+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.8.0 <3.0.0"
|
||||
|
|
Loading…
Reference in New Issue