diff --git a/README.md b/README.md
index 2f820e9b..8f87e3ad 100644
--- a/README.md
+++ b/README.md
@@ -128,9 +128,16 @@ follow [this guide](https://developer.apple.com/help/account/configure-app-capab
AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
// create a new DeezerSourceManager with the master decryption key and register it
-playerManager.registerSourceManager(new DeezerSourceManager("...");
+playerManager.registerSourceManager(new DeezerSourceManager("...", "your arl", formats);
```
+
+How to get deezer arl cookie
+
+Use google to find a guide on how to get the arl cookie. It's not that hard.
+
+
+
#### Yandex Music
@@ -212,6 +219,8 @@ To get your Spotify spDc cookie go [here](#spotify)
To get your Apple Music api token go [here](#apple-music)
+To get your Deezer arl cookie go [here](#deezer)
+
To get your Yandex Music access token go [here](#yandex-music)
(YES `plugins` IS AT ROOT IN THE YAML)
@@ -258,6 +267,8 @@ plugins:
albumLoadLimit: 6 # The number of pages at 300 tracks each
deezer:
masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else)
+ arl: "your deezer arl" # the arl cookie used for accessing the deezer api
+ formats: [ "FLAC", "MP3_320", "MP3_256", "MP3_128", "MP3_64", "AAC_64" ] # the formats you want to use for the deezer tracks. "FLAC", "MP3_320", "MP3_256" & "AAC_64" are only available for premium users and require a valid arl
yandexmusic:
accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music
playlistLoadLimit: 1 # The number of pages at 100 tracks each
diff --git a/application.example.yml b/application.example.yml
index a8cac10c..e3566c2c 100644
--- a/application.example.yml
+++ b/application.example.yml
@@ -33,6 +33,8 @@ plugins:
albumLoadLimit: 6 # The number of pages at 300 tracks each
deezer:
masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else)
+ arl: "your deezer arl" # the arl cookie used for accessing the deezer api
+ formats: [ "FLAC", "MP3_320", "MP3_256", "MP3_128", "MP3_64", "AAC_64" ] # the formats you want to use for the deezer tracks. "FLAC", "MP3_320", "MP3_256" & "AAC_64" are only available for premium users and require a valid arl
yandexmusic:
accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music
playlistLoadLimit: 1 # The number of pages at 100 tracks each
diff --git a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java
index ef7d7266..0549046f 100644
--- a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java
+++ b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java
@@ -57,6 +57,7 @@ public class DeezerAudioSourceManager extends ExtendedAudioSourceManager impleme
private final String masterDecryptionKey;
private final String arl;
+ private final DeezerAudioTrack.TrackFormat[] formats;
private final HttpInterfaceManager httpInterfaceManager;
private Tokens tokens;
@@ -65,15 +66,31 @@ public DeezerAudioSourceManager(String masterDecryptionKey) {
}
public DeezerAudioSourceManager(String masterDecryptionKey, @Nullable String arl) {
+ this(masterDecryptionKey, arl, null);
+ }
+
+ public DeezerAudioSourceManager(String masterDecryptionKey, @Nullable String arl, @Nullable DeezerAudioTrack.TrackFormat[] formats) {
if (masterDecryptionKey == null || masterDecryptionKey.isEmpty()) {
throw new IllegalArgumentException("Deezer master key must be set");
}
this.masterDecryptionKey = masterDecryptionKey;
this.arl = arl != null && arl.isEmpty() ? null : arl;
+ this.formats = formats != null && formats.length > 0 ? formats : DeezerAudioTrack.TrackFormat.DEFAULT_FORMATS;
this.httpInterfaceManager = HttpClientTools.createCookielessThreadLocalManager();
}
+ static void checkResponse(JsonBrowser json, String message) throws IllegalStateException {
+ if (json == null) {
+ throw new IllegalStateException(message + "No response");
+ }
+ var errors = json.get("data").index(0).get("errors").values();
+ if (!errors.isEmpty()) {
+ var errorsStr = errors.stream().map(error -> error.get("code").text() + ": " + error.get("message").text()).collect(Collectors.joining(", "));
+ throw new IllegalStateException(message + errorsStr);
+ }
+ }
+
private void refreshSession() throws IOException {
var getSessionID = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.ping&input=3&api_version=1.0&api_token=");
var json = LavaSrcTools.fetchResponseAsJson(this.getHttpInterface(), getSessionID);
@@ -100,17 +117,6 @@ public Tokens getTokens() throws IOException {
return this.tokens;
}
- static void checkResponse(JsonBrowser json, String message) throws IllegalStateException {
- if (json == null) {
- throw new IllegalStateException(message + "No response");
- }
- var errors = json.get("data").index(0).get("errors").values();
- if (!errors.isEmpty()) {
- var errorsStr = errors.stream().map(error -> error.get("code").text() + ": " + error.get("message").text()).collect(Collectors.joining(", "));
- throw new IllegalStateException(message + errorsStr);
- }
- }
-
@NotNull
@Override
public String getSourceName() {
@@ -405,12 +411,12 @@ private AudioItem getAlbum(String id, boolean preview) throws IOException {
}
return new DeezerAudioPlaylist(json.get("title").text(),
- this.parseTracks(tracks, preview),
- DeezerAudioPlaylist.Type.ALBUM,
- json.get("link").text(),
- artworkUrl,
- author,
- (int) json.get("nb_tracks").asLong(0));
+ this.parseTracks(tracks, preview),
+ DeezerAudioPlaylist.Type.ALBUM,
+ json.get("link").text(),
+ artworkUrl,
+ author,
+ (int) json.get("nb_tracks").asLong(0));
}
private AudioItem getTrack(String id, boolean preview) throws IOException {
@@ -434,12 +440,12 @@ private AudioItem getPlaylist(String id, boolean preview) throws IOException {
var tracks = this.getJson(PUBLIC_API_BASE + "/playlist/" + id + "/tracks?limit=10000");
return new DeezerAudioPlaylist(json.get("title").text(),
- this.parseTracks(tracks, preview),
- DeezerAudioPlaylist.Type.PLAYLIST,
- json.get("link").text(),
- artworkUrl,
- author,
- (int) json.get("nb_tracks").asLong(0));
+ this.parseTracks(tracks, preview),
+ DeezerAudioPlaylist.Type.PLAYLIST,
+ json.get("link").text(),
+ artworkUrl,
+ author,
+ (int) json.get("nb_tracks").asLong(0));
}
private AudioItem getArtist(String id, boolean preview) throws IOException {
@@ -491,6 +497,10 @@ public String getArl() {
return this.arl;
}
+ public DeezerAudioTrack.TrackFormat[] getFormats() {
+ return this.formats;
+ }
+
public HttpInterface getHttpInterface() {
return this.httpInterfaceManager.getInterface();
}
diff --git a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java
index 27500343..8472e656 100644
--- a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java
+++ b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java
@@ -4,8 +4,8 @@
import com.github.topi314.lavasrc.LavaSrcTools;
import com.sedmelluq.discord.lavaplayer.container.flac.FlacAudioTrack;
import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3AudioTrack;
+import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegAudioTrack;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
-import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
import com.sedmelluq.discord.lavaplayer.tools.Units;
@@ -30,8 +30,9 @@
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.function.BiFunction;
-import java.util.stream.Collectors;
public class DeezerAudioTrack extends ExtendedAudioTrack {
@@ -48,104 +49,83 @@ public DeezerAudioTrack(AudioTrackInfo trackInfo, String albumName, String album
this.cookieStore = new BasicCookieStore();
}
- private JsonBrowser getJsonResponse(HttpUriRequest request, boolean includeArl) {
+ private static String formatFormats(TrackFormat[] formats) {
+ var strFormats = new ArrayList();
+ for (var format : formats) {
+ strFormats.add("{\"cipher\":\"BF_CBC_STRIPE\",\"format\":\"" + format.name() + "\"}");
+ }
+ return String.join(",", strFormats);
+ }
+
+ private JsonBrowser getJsonResponse(HttpUriRequest request, boolean useArl) throws IOException {
try (HttpInterface httpInterface = this.sourceManager.getHttpInterface()) {
httpInterface.getContext().setRequestConfig(RequestConfig.custom().setCookieSpec("standard").build());
httpInterface.getContext().setCookieStore(cookieStore);
- if (includeArl && this.sourceManager.getArl() != null) {
+ if (useArl && this.sourceManager.getArl() != null) {
request.setHeader("Cookie", "arl=" + this.sourceManager.getArl());
}
return LavaSrcTools.fetchResponseAsJson(httpInterface, request);
- } catch (IOException e) {
- throw ExceptionTools.toRuntimeException(e);
}
}
- private String getSessionId() {
- final HttpPost getSessionID = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.ping&input=3&api_version=1.0&api_token=");
- final JsonBrowser sessionIdJson = this.getJsonResponse(getSessionID, false);
-
- this.checkResponse(sessionIdJson, "Failed to get session ID: ");
- if (sessionIdJson.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) {
- throw new IllegalStateException("Failed to get session ID");
- }
+ private String getSessionId() throws IOException {
+ var getSessionID = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.ping&input=3&api_version=1.0&api_token=");
+ var sessionIdJson = this.getJsonResponse(getSessionID, false);
+ DeezerAudioSourceManager.checkResponse(sessionIdJson, "Failed to get session ID: ");
return sessionIdJson.get("results").get("SESSION").text();
}
- private JsonBrowser generateLicenceToken(boolean useArl) {
- final HttpGet request = new HttpGet(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.getUserData&input=3&api_version=1.0&api_token=");
+ private LicenseToken generateLicenceToken(boolean useArl) throws IOException {
+ var request = new HttpGet(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.getUserData&input=3&api_version=1.0&api_token=");
// session ID is not needed with ARL and vice-versa.
if (!useArl || this.sourceManager.getArl() == null) {
request.setHeader("Cookie", "sid=" + this.getSessionId());
}
- return this.getJsonResponse(request, useArl);
- }
+ var json = this.getJsonResponse(request, useArl);
+ DeezerAudioSourceManager.checkResponse(json, "Failed to get user token: ");
- public SourceWithFormat getSource(boolean tryFlac, boolean isRetry) throws URISyntaxException {
- var json = this.generateLicenceToken(tryFlac);
- this.checkResponse(json, "Failed to get user token: ");
+ return new LicenseToken(
+ json.get("results").get("USER").get("OPTIONS").get("license_token").text(),
+ json.get("results").get("checkForm").text()
+ );
+ }
- var userLicenseToken = json.get("results").get("USER").get("OPTIONS").get("license_token").text();
- var apiToken = json.get("results").get("checkForm").text();
+ public SourceWithFormat getSource(boolean useArl, boolean isRetry) throws IOException, URISyntaxException {
+ var licenseToken = this.generateLicenceToken(useArl);
- var getTrackToken = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=song.getData&input=3&api_version=1.0&api_token=" + apiToken);
+ var getTrackToken = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=song.getData&input=3&api_version=1.0&api_token=" + licenseToken.apiToken);
getTrackToken.setEntity(new StringEntity("{\"sng_id\":\"" + this.trackInfo.identifier + "\"}", ContentType.APPLICATION_JSON));
- var trackTokenJson = this.getJsonResponse(getTrackToken, tryFlac);
-
- this.checkResponse(trackTokenJson, "Failed to get track token: ");
+ var trackTokenJson = this.getJsonResponse(getTrackToken, useArl);
+ DeezerAudioSourceManager.checkResponse(trackTokenJson, "Failed to get track token: ");
if (trackTokenJson.get("error").get("VALID_TOKEN_REQUIRED").text() != null && !isRetry) {
// "error":{"VALID_TOKEN_REQUIRED":"Invalid CSRF token"}
// seems to indicate an invalid API token?
- return this.getSource(tryFlac, true);
- }
-
- if (tryFlac && trackTokenJson.get("results").get("FILESIZE_FLAC").asLong(0) == 0) {
- // no flac format available.
- return this.getSource(false, false);
+ return this.getSource(useArl, true);
}
var trackToken = trackTokenJson.get("results").get("TRACK_TOKEN").text();
+
var getMediaURL = new HttpPost(DeezerAudioSourceManager.MEDIA_BASE + "/get_url");
+ getMediaURL.setEntity(new StringEntity("{\"license_token\":\"" + licenseToken.userLicenseToken + "\",\"media\":[{\"type\":\"FULL\",\"formats\":[" + formatFormats(this.sourceManager.getFormats()) + "]}],\"track_tokens\": [\"" + trackToken + "\"]}", ContentType.APPLICATION_JSON));
- getMediaURL.setEntity(new StringEntity("{\"license_token\":\"" + userLicenseToken + "\",\"media\":[{\"type\":\"FULL\",\"formats\":[{\"cipher\":\"BF_CBC_STRIPE\",\"format\":\"" + (tryFlac ? "FLAC" : "MP3_128") + "\"}]}],\"track_tokens\": [\"" + trackToken + "\"]}", ContentType.APPLICATION_JSON));
- json = this.getJsonResponse(getMediaURL, tryFlac);
-
- try {
- this.checkResponse(json, "Failed to get media URL: ");
- } catch (IllegalStateException e) {
- // error code 2000 = failed to decode track token
- if (e.getMessage().contains("2000:") && !isRetry) {
- return this.getSource(tryFlac, true);
- } else if (tryFlac) {
- cookieStore.clear();
- return this.getSource(false, false); // Try again but for MP3_128.
- } else {
- throw e;
+ var json = this.getJsonResponse(getMediaURL, useArl);
+ for (var error : json.get("data").get("errors").values()) {
+ if (error.get("code").asLong(0) == 2000) {
+ // error code 2000 = failed to decode track token
+ return this.getSource(useArl, true);
}
}
+ DeezerAudioSourceManager.checkResponse(json, "Failed to get media URL: ");
return SourceWithFormat.fromResponse(json, trackTokenJson);
}
- private void checkResponse(JsonBrowser json, String message) throws IllegalStateException {
- if (json == null) {
- throw new IllegalStateException(message + "No response");
- }
-
- var errors = json.get("data").index(0).get("errors").values();
-
- if (!errors.isEmpty()) {
- var errorsStr = errors.stream().map(error -> error.get("code").text() + ": " + error.get("message").text()).collect(Collectors.joining(", "));
- throw new IllegalStateException(message + errorsStr);
- }
- }
-
public byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException {
var md5 = Hex.encodeHex(MessageDigest.getInstance("MD5").digest(this.trackInfo.identifier.getBytes()), true);
var master_key = this.sourceManager.getMasterDecryptionKey().getBytes();
@@ -168,12 +148,12 @@ public void process(LocalAudioTrackExecutor executor) throws Exception {
try (var stream = new PersistentHttpStream(httpInterface, new URI(this.previewUrl), this.trackInfo.length)) {
processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor);
}
- } else {
- SourceWithFormat source = this.getSource(this.sourceManager.getArl() != null, false);
+ return;
+ }
- try (var stream = new DeezerPersistentHttpStream(httpInterface, source.url, source.contentLength, this.getTrackDecryptionKey())) {
- processDelegate(source.getTrackFactory().apply(this.trackInfo, stream), executor);
- }
+ var source = this.getSource(this.sourceManager.getArl() != null, false);
+ try (var stream = new DeezerPersistentHttpStream(httpInterface, source.url, source.contentLength, this.getTrackDecryptionKey())) {
+ processDelegate(source.format.trackFactory.apply(this.trackInfo, stream), executor);
}
}
}
@@ -188,22 +168,70 @@ public AudioSourceManager getSourceManager() {
return this.sourceManager;
}
+ public enum TrackFormat {
+ FLAC(true, FlacAudioTrack::new),
+ MP3_320(true, Mp3AudioTrack::new),
+ MP3_256(true, Mp3AudioTrack::new),
+ MP3_128(false, Mp3AudioTrack::new),
+ MP3_64(false, Mp3AudioTrack::new),
+ AAC_64(true, MpegAudioTrack::new); // not sure if this one is so better to be safe.
+
+ private boolean isPremiumFormat;
+ private BiFunction trackFactory;
+
+ public static final TrackFormat[] DEFAULT_FORMATS = new TrackFormat[]{MP3_128, MP3_64};
+
+ TrackFormat(boolean isPremiumFormat, BiFunction trackFactory) {
+ this.isPremiumFormat = isPremiumFormat;
+ this.trackFactory = trackFactory;
+ }
+
+ public static TrackFormat from(String format) {
+ return Arrays.stream(TrackFormat.values())
+ .filter(it -> it.name().equals(format))
+ .findFirst()
+ .orElse(null);
+ }
+ }
+
+ private static class LicenseToken {
+ private final String userLicenseToken;
+ private final String apiToken;
+
+ private LicenseToken(String userLicenseToken, String apiToken) {
+ this.userLicenseToken = userLicenseToken;
+ this.apiToken = apiToken;
+ }
+ }
+
public static class SourceWithFormat {
private final URI url;
- private final String format;
+ private final TrackFormat format;
private final long contentLength;
- private SourceWithFormat(String url, String format, long contentLength) throws URISyntaxException {
+ private SourceWithFormat(String url, TrackFormat format, long contentLength) throws URISyntaxException {
this.url = new URI(url);
this.format = format;
this.contentLength = contentLength;
}
+ private static SourceWithFormat fromResponse(JsonBrowser json, JsonBrowser trackJson) throws URISyntaxException {
+ var media = json.get("data").index(0).get("media").index(0);
+ if (media.isNull()) {
+ return null;
+ }
+
+ var format = media.get("format").text();
+ var url = media.get("sources").index(0).get("url").text();
+ var contentLength = trackJson.get("results").get("FILESIZE_" + format).asLong(Units.CONTENT_LENGTH_UNKNOWN);
+ return new SourceWithFormat(url, TrackFormat.from(format), contentLength);
+ }
+
public URI getUrl() {
return this.url;
}
- public String getFormat() {
+ public TrackFormat getFormat() {
return this.format;
}
@@ -211,22 +239,5 @@ public long getContentLength() {
return this.contentLength;
}
- private BiFunction getTrackFactory() {
- return this.format.equals("FLAC") ? FlacAudioTrack::new : Mp3AudioTrack::new;
- }
-
- private static SourceWithFormat fromResponse(JsonBrowser json, JsonBrowser trackJson) throws URISyntaxException {
- JsonBrowser media = json.get("data").index(0).get("media").index(0);
- JsonBrowser sources = media.get("sources");
-
- if (media.isNull()) {
- return null;
- }
-
- String format = media.get("format").text();
- String url = sources.index(0).get("url").text();
- long contentLength = trackJson.get("results").get("FILESIZE_" + format).asLong(Units.CONTENT_LENGTH_UNKNOWN);
- return new SourceWithFormat(url, format, contentLength);
- }
}
}
diff --git a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/DeezerConfig.java b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/DeezerConfig.java
index 8f1418bd..806dcd47 100644
--- a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/DeezerConfig.java
+++ b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/DeezerConfig.java
@@ -1,5 +1,6 @@
package com.github.topi314.lavasrc.plugin;
+import com.github.topi314.lavasrc.deezer.DeezerAudioTrack;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -9,21 +10,30 @@ public class DeezerConfig {
private String masterDecryptionKey;
private String arl;
+ private DeezerAudioTrack.TrackFormat[] formats;
public String getMasterDecryptionKey() {
return this.masterDecryptionKey;
}
- public String getArl() {
- return this.arl;
- }
-
public void setMasterDecryptionKey(String masterDecryptionKey) {
this.masterDecryptionKey = masterDecryptionKey;
}
+ public String getArl() {
+ return this.arl;
+ }
+
public void setArl(String arl) {
this.arl = arl;
}
+ public DeezerAudioTrack.TrackFormat[] getFormats() {
+ return this.formats;
+ }
+
+ public void setFormats(DeezerAudioTrack.TrackFormat[] formats) {
+ this.formats = formats;
+ }
+
}
diff --git a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java
index 023edc95..bab57f43 100644
--- a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java
+++ b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java
@@ -60,7 +60,7 @@ public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, Ly
}
}
if (sourcesConfig.isDeezer() || lyricsSourcesConfig.isDeezer()) {
- this.deezer = new DeezerAudioSourceManager(deezerConfig.getMasterDecryptionKey(), deezerConfig.getArl());
+ this.deezer = new DeezerAudioSourceManager(deezerConfig.getMasterDecryptionKey(), deezerConfig.getArl(), deezerConfig.getFormats());
}
if (sourcesConfig.isYandexMusic() || lyricsSourcesConfig.isYandexMusic()) {
this.yandexMusic = new YandexMusicSourceManager(yandexMusicConfig.getAccessToken());