diff --git a/README.md b/README.md
index f2be0954..caee9c92 100644
--- a/README.md
+++ b/README.md
@@ -171,10 +171,25 @@ AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
// create a new DeezerSourceManager with the master decryption key and register it
-var deezer = new DeezerSourceManager("...");
+var deezer = new DeezerSourceManager("the master decryption key", "your arl", formats);
playerManager.registerSourceManager(deezer);
```
+
+How to get deezer master decryption key
+
+Use google.
+
+
+
+
+How to get deezer arl cookie
+
+Use google to find a guide on how to get the arl cookie. It's not that hard.
+
+
+
+
#### LavaLyrics
Click to expand
@@ -312,6 +327,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)
@@ -358,6 +375,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 8696972b..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
@@ -56,15 +56,39 @@ public class DeezerAudioSourceManager extends ExtendedAudioSourceManager impleme
private static final Logger log = LoggerFactory.getLogger(DeezerAudioSourceManager.class);
private final String masterDecryptionKey;
+ private final String arl;
+ private final DeezerAudioTrack.TrackFormat[] formats;
private final HttpInterfaceManager httpInterfaceManager;
private Tokens tokens;
public DeezerAudioSourceManager(String masterDecryptionKey) {
+ this(masterDecryptionKey, null);
+ }
+
+ 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.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
+ 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 {
@@ -93,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() {
@@ -398,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 {
@@ -427,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 {
@@ -479,6 +492,15 @@ public String getMasterDecryptionKey() {
return this.masterDecryptionKey;
}
+ @Nullable
+ 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 d1ddf4cf..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
@@ -2,29 +2,42 @@
import com.github.topi314.lavasrc.ExtendedAudioTrack;
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.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
+import com.sedmelluq.discord.lavaplayer.tools.Units;
+import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
+import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack;
import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor;
import org.apache.commons.codec.binary.Hex;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCookieStore;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.function.BiFunction;
public class DeezerAudioTrack extends ExtendedAudioTrack {
private final DeezerAudioSourceManager sourceManager;
+ private final CookieStore cookieStore;
public DeezerAudioTrack(AudioTrackInfo trackInfo, DeezerAudioSourceManager sourceManager) {
this(trackInfo, null, null, null, null, null, false, sourceManager);
@@ -33,50 +46,87 @@ public DeezerAudioTrack(AudioTrackInfo trackInfo, DeezerAudioSourceManager sourc
public DeezerAudioTrack(AudioTrackInfo trackInfo, String albumName, String albumUrl, String artistUrl, String artistArtworkUrl, String previewUrl, boolean isPreview, DeezerAudioSourceManager sourceManager) {
super(trackInfo, albumName, albumUrl, artistUrl, artistArtworkUrl, previewUrl, isPreview);
this.sourceManager = sourceManager;
+ this.cookieStore = new BasicCookieStore();
}
- private URI getTrackMediaURI() throws IOException, URISyntaxException {
- var getSessionID = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.ping&input=3&api_version=1.0&api_token=");
- var json = LavaSrcTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getSessionID);
+ 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);
+ }
- this.checkResponse(json, "Failed to get session ID: ");
- var sessionID = json.get("results").get("SESSION").text();
+ 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);
- var getUserToken = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.getUserData&input=3&api_version=1.0&api_token=");
- getUserToken.setHeader("Cookie", "sid=" + sessionID);
- json = LavaSrcTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getUserToken);
+ if (useArl && this.sourceManager.getArl() != null) {
+ request.setHeader("Cookie", "arl=" + this.sourceManager.getArl());
+ }
- this.checkResponse(json, "Failed to get user token: ");
- var userLicenseToken = json.get("results").get("USER").get("OPTIONS").get("license_token").text();
- var apiToken = json.get("results").get("checkForm").text();
+ return LavaSrcTools.fetchResponseAsJson(httpInterface, request);
+ }
+ }
- var getTrackToken = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=song.getData&input=3&api_version=1.0&api_token=" + apiToken);
- getTrackToken.setEntity(new StringEntity("{\"sng_id\":\"" + this.trackInfo.identifier + "\"}", ContentType.APPLICATION_JSON));
- json = LavaSrcTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getTrackToken);
+ 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);
- this.checkResponse(json, "Failed to get track token: ");
- var trackToken = json.get("results").get("TRACK_TOKEN").text();
+ DeezerAudioSourceManager.checkResponse(sessionIdJson, "Failed to get session ID: ");
+ return sessionIdJson.get("results").get("SESSION").text();
+ }
- var getMediaURL = new HttpPost(DeezerAudioSourceManager.MEDIA_BASE + "/get_url");
- getMediaURL.setEntity(new StringEntity("{\"license_token\":\"" + userLicenseToken + "\",\"media\": [{\"type\": \"FULL\",\"formats\": [{\"cipher\": \"BF_CBC_STRIPE\", \"format\": \"MP3_128\"}]}],\"track_tokens\": [\"" + trackToken + "\"]}", ContentType.APPLICATION_JSON));
- json = LavaSrcTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getMediaURL);
+ 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=");
- this.checkResponse(json, "Failed to get media URL: ");
- return new URI(json.get("data").index(0).get("media").index(0).get("sources").index(0).get("url").text());
+ // session ID is not needed with ARL and vice-versa.
+ if (!useArl || this.sourceManager.getArl() == null) {
+ request.setHeader("Cookie", "sid=" + this.getSessionId());
+ }
+
+ var json = this.getJsonResponse(request, useArl);
+ DeezerAudioSourceManager.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()
+ );
}
- private void checkResponse(JsonBrowser json, String message) throws IllegalStateException {
- if (json == null) {
- throw new IllegalStateException(message + "No response");
+ 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=" + licenseToken.apiToken);
+ getTrackToken.setEntity(new StringEntity("{\"sng_id\":\"" + this.trackInfo.identifier + "\"}", ContentType.APPLICATION_JSON));
+ 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(useArl, true);
}
- 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);
+
+ 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));
+
+ 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 byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException {
+ 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();
@@ -94,13 +144,16 @@ public void process(LocalAudioTrackExecutor executor) throws Exception {
if (this.previewUrl == null) {
throw new FriendlyException("No preview url found", FriendlyException.Severity.COMMON, new IllegalArgumentException());
}
+
try (var stream = new PersistentHttpStream(httpInterface, new URI(this.previewUrl), this.trackInfo.length)) {
processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor);
}
- } else {
- try (var stream = new DeezerPersistentHttpStream(httpInterface, this.getTrackMediaURI(), this.trackInfo.length, this.getTrackDecryptionKey())) {
- processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor);
- }
+ return;
+ }
+
+ 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);
}
}
}
@@ -115,4 +168,76 @@ 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 TrackFormat format;
+ private final long contentLength;
+
+ 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 TrackFormat getFormat() {
+ return this.format;
+ }
+
+ public long getContentLength() {
+ return this.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 74de84ab..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;
@@ -8,6 +9,8 @@
public class DeezerConfig {
private String masterDecryptionKey;
+ private String arl;
+ private DeezerAudioTrack.TrackFormat[] formats;
public String getMasterDecryptionKey() {
return this.masterDecryptionKey;
@@ -17,4 +20,20 @@ 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 3cc28399..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());
+ this.deezer = new DeezerAudioSourceManager(deezerConfig.getMasterDecryptionKey(), deezerConfig.getArl(), deezerConfig.getFormats());
}
if (sourcesConfig.isYandexMusic() || lyricsSourcesConfig.isYandexMusic()) {
this.yandexMusic = new YandexMusicSourceManager(yandexMusicConfig.getAccessToken());