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());