From 1dd7d6bdb90a52bb082bd7072cc0cb73163148a2 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Mon, 28 Nov 2022 03:38:59 +0100 Subject: [PATCH 01/11] implement audio track & playlist custom json fields --- build.gradle | 2 +- main/build.gradle | 2 +- .../applemusic/AppleMusicAudioPlaylist.java | 39 ++ .../applemusic/AppleMusicAudioTrack.java | 14 +- .../applemusic/AppleMusicSourceManager.java | 502 ++++++++--------- .../lavasrc/deezer/DeezerAudioPlaylist.java | 39 ++ .../deezer/DeezerAudioSourceManager.java | 425 ++++++++------- .../lavasrc/deezer/DeezerAudioTrack.java | 164 +++--- .../deezer/DeezerPersistentHttpStream.java | 121 ++--- .../lavasrc/deezer/PersistentHttpStream.java | 312 ----------- .../mirror/MirroringAudioSourceManager.java | 68 +-- .../lavasrc/mirror/MirroringAudioTrack.java | 210 ++++---- .../mirror/TrackNotFoundException.java | 2 +- .../lavasrc/spotify/SpotifyAudioPlaylist.java | 39 ++ .../lavasrc/spotify/SpotifyAudioTrack.java | 14 +- .../lavasrc/spotify/SpotifySourceManager.java | 509 +++++++++--------- .../yandexmusic/YandexMusicAudioPlaylist.java | 39 ++ .../yandexmusic/YandexMusicAudioTrack.java | 104 ++-- .../yandexmusic/YandexMusicSourceManager.java | 424 ++++++++------- plugin/build.gradle | 9 +- .../lavasrc/plugin/AppleMusicConfig.java | 28 +- .../lavasrc/plugin/DeezerConfig.java | 14 +- .../lavasrc/plugin/LavaSrcConfig.java | 20 +- .../lavasrc/plugin/LavaSrcJsonAppender.java | 86 +++ .../lavasrc/plugin/LavaSrcPlugin.java | 80 +-- .../lavasrc/plugin/SourcesConfig.java | 72 +-- .../lavasrc/plugin/SpotifyConfig.java | 42 +- .../lavasrc/plugin/YandexMusicConfig.java | 14 +- .../lavalink-plugins/lavasrc.properties | 2 +- 29 files changed, 1680 insertions(+), 1716 deletions(-) create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java delete mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java create mode 100644 plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java diff --git a/build.gradle b/build.gradle index 3c2e7167..31f6558a 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ allprojects { subprojects { apply plugin: "java" - version "3.1.6" + version "3.2.0" sourceCompatibility = 11 compileJava.options.encoding = "UTF-8" diff --git a/main/build.gradle b/main/build.gradle index 4daa414a..02f1eba1 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -6,7 +6,7 @@ plugins { var moduleName = "lavasrc" dependencies { - compileOnly "com.github.walkyst:lavaplayer-fork:1.3.98.4" + compileOnly "com.github.walkyst:lavaplayer-fork:1.3.99.1" implementation "org.jsoup:jsoup:1.14.3" implementation "commons-io:commons-io:2.6" } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java new file mode 100644 index 00000000..70166218 --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.applemusic; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class AppleMusicAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public AppleMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java index c27b2824..380ad7f4 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java @@ -6,13 +6,13 @@ public class AppleMusicAudioTrack extends MirroringAudioTrack { - public AppleMusicAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, AppleMusicSourceManager sourceManager) { - super(trackInfo, isrc, artworkURL, sourceManager); - } + public AppleMusicAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, AppleMusicSourceManager sourceManager) { + super(trackInfo, isrc, artworkURL, sourceManager); + } - @Override - protected AudioTrack makeShallowClone() { - return new AppleMusicAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (AppleMusicSourceManager) this.sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new AppleMusicAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (AppleMusicSourceManager) this.sourceManager); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java index 588a2bc9..011e5bcf 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java @@ -34,250 +34,262 @@ public class AppleMusicSourceManager extends MirroringAudioSourceManager implements HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?[a-zA-Z]{2}/)?(?album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?[a-zA-Z\\d\\-.]+)(\\?i=(?\\d+))?"); - public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?ey[\\w.-]+)\""); - public static final String SEARCH_PREFIX = "amsearch:"; - public static final int MAX_PAGE_ITEMS = 300; - public static final String API_BASE = "https://api.music.apple.com/v1/"; - private static final Logger log = LoggerFactory.getLogger(AppleMusicSourceManager.class); - private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - private final String countryCode; - private String token; - private String origin; - private Instant tokenExpire; - - public AppleMusicSourceManager(String[] providers, String mediaAPIToken, String countryCode, AudioPlayerManager audioPlayerManager) { - super(providers, audioPlayerManager); - this.token = mediaAPIToken; - try { - this.parseTokenData(); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot parse token for expire date and origin", e); - } - if (countryCode == null || countryCode.isEmpty()) { - this.countryCode = "us"; - } else { - this.countryCode = countryCode; - } - } - - @Override - public String getSourceName() { - return "applemusic"; - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new AppleMusicAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (!matcher.find()) { - return null; - } - - var countryCode = matcher.group("countrycode"); - var id = matcher.group("identifier"); - switch (matcher.group("type")) { - case "song": - return this.getSong(id, countryCode); - - case "album": - var id2 = matcher.group("identifier2"); - if (id2 == null || id2.isEmpty()) { - return this.getAlbum(id, countryCode); - } - return this.getSong(id2, countryCode); - - case "playlist": - return this.getPlaylist(id, countryCode); - - case "artist": - return this.getArtist(id, countryCode); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public void parseTokenData() throws IOException { - if (this.token == null || this.token.isEmpty()) { - return; - } - var json = JsonBrowser.parse(new String(Base64.getDecoder().decode(this.token.split("\\.")[1]))); - this.tokenExpire = Instant.ofEpochSecond(json.get("exp").asLong(0)); - this.origin = json.get("root_https_origin").index(0).text(); - } - - public void requestToken() throws IOException { - var request = new HttpGet("https://music.apple.com"); - String tokenScriptURL; - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - var document = Jsoup.parse(response.getEntity().getContent(), null, ""); - var element = document.selectFirst("script[type=module][src~=/assets/index.*.js]"); - if (element == null) { - throw new IllegalStateException("Cannot find token script element"); - } - tokenScriptURL = element.attr("src"); - } - if (tokenScriptURL.isEmpty()) { - throw new IllegalStateException("Cannot find token script url"); - } - request = new HttpGet("https://music.apple.com" + tokenScriptURL); - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - var tokenScript = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript); - if (!tokenMatcher.find()) { - throw new IllegalStateException("Cannot find token in script"); - } - this.token = tokenMatcher.group("token"); - } - this.parseTokenData(); - } - - public String getToken() throws IOException { - if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { - this.requestToken(); - } - return this.token; - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.addHeader("Authorization", "Bearer " + this.getToken()); - if (this.origin != null && !this.origin.isEmpty()) { - request.addHeader("Origin", "https://" + this.origin); - } - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - public AudioItem getSearch(String query) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/search?term=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&limit=" + 25); - if (json == null || json.get("results").get("songs").get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist("Apple Music Search: " + query, parseTracks(json.get("results").get("songs")), null, true); - } - - public AudioItem getAlbum(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); - offset += MAX_PAGE_ITEMS; - - tracks.addAll(parseTracks(page)); - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, null, false); - } - - public AudioItem getPlaylist(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); - offset += MAX_PAGE_ITEMS; - - tracks.addAll(parseTracks(page)); - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, null, false); - } - - public AudioItem getArtist(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/artists/" + id + "/view/top-songs"); - if (json == null || json.get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("artistName").text() + "'s Top Tracks", parseTracks(json), null, false); - } - - public AudioItem getSong(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/songs/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - return parseTrack(json.get("data").index(0)); - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var value : json.get("data").values()) { - tracks.add(this.parseTrack(value)); - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - var attributes = json.get("attributes"); - var artwork = attributes.get("artwork"); - return new AppleMusicAudioTrack( - new AudioTrackInfo( - attributes.get("name").text(), - attributes.get("artistName").text(), - attributes.get("durationInMillis").asLong(0), - json.get("id").text(), - false, - attributes.get("url").text() - ), - attributes.get("isrc").text(), - artwork.get("url").text().replace("{w}", artwork.get("width").text()).replace("{h}", artwork.get("height").text()), - this - ); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?[a-zA-Z]{2}/)?(?album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?[a-zA-Z\\d\\-.]+)(\\?i=(?\\d+))?"); + public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?ey[\\w.-]+)\""); + public static final String SEARCH_PREFIX = "amsearch:"; + public static final int MAX_PAGE_ITEMS = 300; + public static final String API_BASE = "https://api.music.apple.com/v1/"; + private static final Logger log = LoggerFactory.getLogger(AppleMusicSourceManager.class); + private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + private final String countryCode; + private String token; + private String origin; + private Instant tokenExpire; + + public AppleMusicSourceManager(String[] providers, String mediaAPIToken, String countryCode, AudioPlayerManager audioPlayerManager) { + super(providers, audioPlayerManager); + this.token = mediaAPIToken; + try { + this.parseTokenData(); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot parse token for expire date and origin", e); + } + if (countryCode == null || countryCode.isEmpty()) { + this.countryCode = "us"; + } else { + this.countryCode = countryCode; + } + } + + @Override + public String getSourceName() { + return "applemusic"; + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new AppleMusicAudioTrack(trackInfo, + DataFormatTools.readNullableText(input), + DataFormatTools.readNullableText(input), + this + ); + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (!matcher.find()) { + return null; + } + + var countryCode = matcher.group("countrycode"); + var id = matcher.group("identifier"); + switch (matcher.group("type")) { + case "song": + return this.getSong(id, countryCode); + + case "album": + var id2 = matcher.group("identifier2"); + if (id2 == null || id2.isEmpty()) { + return this.getAlbum(id, countryCode); + } + return this.getSong(id2, countryCode); + + case "playlist": + return this.getPlaylist(id, countryCode); + + case "artist": + return this.getArtist(id, countryCode); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + public void parseTokenData() throws IOException { + if (this.token == null || this.token.isEmpty()) { + return; + } + var json = JsonBrowser.parse(new String(Base64.getDecoder().decode(this.token.split("\\.")[1]))); + this.tokenExpire = Instant.ofEpochSecond(json.get("exp").asLong(0)); + this.origin = json.get("root_https_origin").index(0).text(); + } + + public void requestToken() throws IOException { + var request = new HttpGet("https://music.apple.com"); + String tokenScriptURL; + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + var document = Jsoup.parse(response.getEntity().getContent(), null, ""); + var element = document.selectFirst("script[type=module][src~=/assets/index.*.js]"); + if (element == null) { + throw new IllegalStateException("Cannot find token script element"); + } + tokenScriptURL = element.attr("src"); + } + if (tokenScriptURL.isEmpty()) { + throw new IllegalStateException("Cannot find token script url"); + } + request = new HttpGet("https://music.apple.com" + tokenScriptURL); + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + var tokenScript = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript); + if (!tokenMatcher.find()) { + throw new IllegalStateException("Cannot find token in script"); + } + this.token = tokenMatcher.group("token"); + } + this.parseTokenData(); + } + + public String getToken() throws IOException { + if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { + this.requestToken(); + } + return this.token; + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.addHeader("Authorization", "Bearer " + this.getToken()); + if (this.origin != null && !this.origin.isEmpty()) { + request.addHeader("Origin", "https://" + this.origin); + } + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + public AudioItem getSearch(String query) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/search?term=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&limit=" + 25); + if (json == null || json.get("results").get("songs").get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + return new BasicAudioPlaylist("Apple Music Search: " + query, parseTracks(json.get("results").get("songs")), null, true); + } + + public AudioItem getAlbum(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); + offset += MAX_PAGE_ITEMS; + + tracks.addAll(parseTracks(page)); + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = this.parseArtworkUrl(json.get("data").index(0).get("attributes").get("artwork")); + var author = json.get("data").index(0).get("attributes").get("artistName").text(); + return new AppleMusicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, "album", id, artworkUrl, author); + } + + public AudioItem getPlaylist(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); + offset += MAX_PAGE_ITEMS; + + tracks.addAll(parseTracks(page)); + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = this.parseArtworkUrl(json.get("data").index(0).get("attributes").get("artwork")); + var author = json.get("data").index(0).get("attributes").get("curatorName").text(); + return new AppleMusicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, "playlist", id, artworkUrl, author); + } + + public AudioItem getArtist(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/artists/" + id + "/view/top-songs"); + if (json == null || json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var jsonArtist = this.getJson(API_BASE + "catalog/" + countryCode + "/artists/" + id); + + var artworkUrl = this.parseArtworkUrl(jsonArtist.get("data").index(0).get("attributes").get("artwork")); + var author = jsonArtist.get("data").index(0).get("attributes").get("name").text(); + return new AppleMusicAudioPlaylist(author + "'s Top Tracks", parseTracks(json), "artist", id, artworkUrl, author); + } + + public AudioItem getSong(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/songs/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + return parseTrack(json.get("data").index(0)); + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var value : json.get("data").values()) { + tracks.add(this.parseTrack(value)); + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + var attributes = json.get("attributes"); + return new AppleMusicAudioTrack( + new AudioTrackInfo( + attributes.get("name").text(), + attributes.get("artistName").text(), + attributes.get("durationInMillis").asLong(0), + json.get("id").text(), + false, + attributes.get("url").text() + ), + attributes.get("isrc").text(), + this.parseArtworkUrl(attributes.get("artwork")), + this + ); + } + + private String parseArtworkUrl(JsonBrowser json) { + return json.get("url").text().replace("{w}", json.get("width").text()).replace("{h}", json.get("height").text()); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java new file mode 100644 index 00000000..901a448e --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.deezer; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class DeezerAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public DeezerAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java index 350cb157..85777bf9 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java @@ -32,213 +32,222 @@ public class DeezerAudioSourceManager implements AudioSourceManager, HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?deezer\\.com/(?[a-zA-Z]{2}/)?(?track|album|playlist|artist)/(?[0-9]+)"); - public static final String SEARCH_PREFIX = "dzsearch:"; - public static final String ISRC_PREFIX = "dzisrc:"; - public static final String SHARE_URL = "https://deezer.page.link/"; - public static final String PUBLIC_API_BASE = "https://api.deezer.com/2.0"; - public static final String PRIVATE_API_BASE = "https://www.deezer.com/ajax/gw-light.php"; - public static final String MEDIA_BASE = "https://media.deezer.com/v1"; - - private static final Logger log = LoggerFactory.getLogger(DeezerAudioSourceManager.class); - - private final String masterDecryptionKey; - - private final HttpInterfaceManager httpInterfaceManager; - - public DeezerAudioSourceManager(String masterDecryptionKey) { - if (masterDecryptionKey == null || masterDecryptionKey.isEmpty()) { - throw new IllegalArgumentException("Deezer master key must be set"); - } - this.masterDecryptionKey = masterDecryptionKey; - this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } - - @Override - public String getSourceName() { - return "deezer"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); - } - - if (reference.identifier.startsWith(ISRC_PREFIX)) { - return this.getTrackByISRC(reference.identifier.substring(ISRC_PREFIX.length())); - } - - // If the identifier is a share URL, we need to follow the redirect to find out the real url behind it - if (reference.identifier.startsWith(SHARE_URL)) { - var request = new HttpGet(reference.identifier); - request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - if (response.getStatusLine().getStatusCode() == 302) { - var location = response.getFirstHeader("Location").getValue(); - if (location.startsWith("https://www.deezer.com/")) { - return this.loadItem(manager, new AudioReference(location, reference.title)); - } - } - return null; - } - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (!matcher.find()) { - return null; - } - - var id = matcher.group("identifier"); - switch (matcher.group("type")) { - case "album": - return this.getAlbum(id); - - case "track": - return this.getTrack(id); - - case "playlist": - return this.getPlaylist(id); - - case "artist": - return this.getArtist(id); - } - - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.setHeader("Accept", "application/json"); - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var track : json.get("data").values()) { - if (!track.get("type").text().equals("track")) { - continue; - } - tracks.add(this.parseTrack(track)); - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - var id = json.get("id").text(); - return new DeezerAudioTrack(new AudioTrackInfo( - json.get("title").text(), - json.get("artist").get("name").text(), - json.get("duration").as(Long.class) * 1000, - id, - false, - "https://deezer.com/track/" + id), - json.get("isrc").text(), - json.get("album").get("cover_xl").text(), - this - ); - } - - private AudioItem getTrackByISRC(String isrc) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/track/isrc:" + isrc); - if (json == null || json.get("id").isNull()) { - return AudioReference.NO_TRACK; - } - return this.parseTrack(json); - } - - private AudioItem getSearch(String query) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8)); - if (json == null || json.get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - - var tracks = this.parseTracks(json); - return new BasicAudioPlaylist("Deezer Search: " + query, tracks, null, true); - } - - private AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/album/" + id); - if (json == null || json.get("tracks").get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), null, false); - } - - private AudioItem getTrack(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/track/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - return this.parseTrack(json); - } - - private AudioItem getPlaylist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/playlist/" + id); - if (json == null || json.get("tracks").get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), null, false); - } - - private AudioItem getArtist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/artist/" + id + "/top?limit=50"); - if (json == null || json.get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("data").index(0).get("artist").get("name").text() + "'s Top Tracks", this.parseTracks(json), null, false); - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var deezerAudioTrack = ((DeezerAudioTrack) track); - DataFormatTools.writeNullableText(output, deezerAudioTrack.getISRC()); - DataFormatTools.writeNullableText(output, deezerAudioTrack.getArtworkURL()); - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new DeezerAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } - - public String getMasterDecryptionKey() { - return this.masterDecryptionKey; - } - - public HttpInterface getHttpInterface() { - return this.httpInterfaceManager.getInterface(); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?deezer\\.com/(?[a-zA-Z]{2}/)?(?track|album|playlist|artist)/(?[0-9]+)"); + public static final String SEARCH_PREFIX = "dzsearch:"; + public static final String ISRC_PREFIX = "dzisrc:"; + public static final String SHARE_URL = "https://deezer.page.link/"; + public static final String PUBLIC_API_BASE = "https://api.deezer.com/2.0"; + public static final String PRIVATE_API_BASE = "https://www.deezer.com/ajax/gw-light.php"; + public static final String MEDIA_BASE = "https://media.deezer.com/v1"; + + private static final Logger log = LoggerFactory.getLogger(DeezerAudioSourceManager.class); + + private final String masterDecryptionKey; + + private final HttpInterfaceManager httpInterfaceManager; + + public DeezerAudioSourceManager(String masterDecryptionKey) { + if (masterDecryptionKey == null || masterDecryptionKey.isEmpty()) { + throw new IllegalArgumentException("Deezer master key must be set"); + } + this.masterDecryptionKey = masterDecryptionKey; + this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "deezer"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); + } + + if (reference.identifier.startsWith(ISRC_PREFIX)) { + return this.getTrackByISRC(reference.identifier.substring(ISRC_PREFIX.length())); + } + + // If the identifier is a share URL, we need to follow the redirect to find out the real url behind it + if (reference.identifier.startsWith(SHARE_URL)) { + var request = new HttpGet(reference.identifier); + request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + if (response.getStatusLine().getStatusCode() == 302) { + var location = response.getFirstHeader("Location").getValue(); + if (location.startsWith("https://www.deezer.com/")) { + return this.loadItem(manager, new AudioReference(location, reference.title)); + } + } + return null; + } + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (!matcher.find()) { + return null; + } + + var id = matcher.group("identifier"); + switch (matcher.group("type")) { + case "album": + return this.getAlbum(id); + + case "track": + return this.getTrack(id); + + case "playlist": + return this.getPlaylist(id); + + case "artist": + return this.getArtist(id); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.setHeader("Accept", "application/json"); + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var track : json.get("data").values()) { + if (!track.get("type").text().equals("track")) { + continue; + } + tracks.add(this.parseTrack(track)); + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + var id = json.get("id").text(); + return new DeezerAudioTrack(new AudioTrackInfo( + json.get("title").text(), + json.get("artist").get("name").text(), + json.get("duration").as(Long.class) * 1000, + id, + false, + "https://deezer.com/track/" + id), + json.get("isrc").text(), + json.get("album").get("cover_xl").text(), + this + ); + } + + private AudioItem getTrackByISRC(String isrc) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/track/isrc:" + isrc); + if (json == null || json.get("id").isNull()) { + return AudioReference.NO_TRACK; + } + return this.parseTrack(json); + } + + private AudioItem getSearch(String query) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8)); + if (json == null || json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var tracks = this.parseTracks(json); + return new BasicAudioPlaylist("Deezer Search: " + query, tracks, null, true); + } + + private AudioItem getAlbum(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/album/" + id); + if (json == null || json.get("tracks").get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("cover_xl").text(); + var author = json.get("contributors").values().get(0).get("name").text(); + return new DeezerAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), "album", id, artworkUrl, author); + } + + private AudioItem getTrack(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/track/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + return this.parseTrack(json); + } + + private AudioItem getPlaylist(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/playlist/" + id); + if (json == null || json.get("tracks").get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("picture_xl").text(); + var author = json.get("creator").get("name").text(); + return new DeezerAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), "playlist", id, artworkUrl, author); + } + + private AudioItem getArtist(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/artist/" + id + "/top?limit=50"); + if (json == null || json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("data").index(0).get("contributors").get("picture_xl").text(); + var author = json.get("data").index(0).get("contributors").get("name").text(); + return new DeezerAudioPlaylist(author + "'s Top Tracks", this.parseTracks(json), "artist", id, artworkUrl, author); + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var deezerAudioTrack = ((DeezerAudioTrack) track); + DataFormatTools.writeNullableText(output, deezerAudioTrack.getISRC()); + DataFormatTools.writeNullableText(output, deezerAudioTrack.getArtworkURL()); + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new DeezerAudioTrack(trackInfo, + DataFormatTools.readNullableText(input), + DataFormatTools.readNullableText(input), + this + ); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } + + public String getMasterDecryptionKey() { + return this.masterDecryptionKey; + } + + public HttpInterface getHttpInterface() { + return this.httpInterfaceManager.getInterface(); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java index 5f986a39..3402815e 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java @@ -20,87 +20,87 @@ public class DeezerAudioTrack extends DelegatedAudioTrack { - private final String isrc; - private final String artworkURL; - private final DeezerAudioSourceManager sourceManager; - - public DeezerAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, DeezerAudioSourceManager sourceManager) { - super(trackInfo); - this.isrc = isrc; - this.artworkURL = artworkURL; - this.sourceManager = sourceManager; - } - - public String getISRC() { - return this.isrc; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getSessionID); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("Failed to get session ID"); - } - var sessionID = json.get("results").get("SESSION").text(); - - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getUserToken); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("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(); - - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getTrackToken); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("Failed to get track token"); - } - var trackToken = json.get("results").get("TRACK_TOKEN").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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getMediaURL); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("Failed to get media URL"); - } - return new URI(json.get("data").index(0).get("media").index(0).get("sources").index(0).get("url").text()); - } - - private byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException { - var md5 = Hex.encodeHex(MessageDigest.getInstance("MD5").digest(this.trackInfo.identifier.getBytes()), true); - var master_key = this.sourceManager.getMasterDecryptionKey().getBytes(); - - var key = new byte[16]; - for (int i = 0; i < 16; i++) { - key[i] = (byte) (md5[i] ^ md5[i + 16] ^ master_key[i]); - } - return key; - } - - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - try (var httpInterface = this.sourceManager.getHttpInterface()) { - try (var stream = new DeezerPersistentHttpStream(httpInterface, this.getTrackMediaURI(), this.trackInfo.length, this.getTrackDecryptionKey())) { - processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); - } - } - } - - @Override - protected AudioTrack makeShallowClone() { - return new DeezerAudioTrack(this.trackInfo, this.isrc, this.artworkURL, this.sourceManager); - } - - @Override - public AudioSourceManager getSourceManager() { - return this.sourceManager; - } + private final String isrc; + private final String artworkURL; + private final DeezerAudioSourceManager sourceManager; + + public DeezerAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, DeezerAudioSourceManager sourceManager) { + super(trackInfo); + this.isrc = isrc; + this.artworkURL = artworkURL; + this.sourceManager = sourceManager; + } + + public String getISRC() { + return this.isrc; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getSessionID); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("Failed to get session ID"); + } + var sessionID = json.get("results").get("SESSION").text(); + + 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getUserToken); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("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(); + + 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getTrackToken); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("Failed to get track token"); + } + var trackToken = json.get("results").get("TRACK_TOKEN").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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getMediaURL); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("Failed to get media URL"); + } + return new URI(json.get("data").index(0).get("media").index(0).get("sources").index(0).get("url").text()); + } + + private byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException { + var md5 = Hex.encodeHex(MessageDigest.getInstance("MD5").digest(this.trackInfo.identifier.getBytes()), true); + var master_key = this.sourceManager.getMasterDecryptionKey().getBytes(); + + var key = new byte[16]; + for (int i = 0; i < 16; i++) { + key[i] = (byte) (md5[i] ^ md5[i + 16] ^ master_key[i]); + } + return key; + } + + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + try (var httpInterface = this.sourceManager.getHttpInterface()) { + try (var stream = new DeezerPersistentHttpStream(httpInterface, this.getTrackMediaURI(), this.trackInfo.length, this.getTrackDecryptionKey())) { + processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); + } + } + } + + @Override + protected AudioTrack makeShallowClone() { + return new DeezerAudioTrack(this.trackInfo, this.isrc, this.artworkURL, this.sourceManager); + } + + @Override + public AudioSourceManager getSourceManager() { + return this.sourceManager; + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java index 500e0b7c..5e9e7197 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java @@ -2,6 +2,7 @@ import com.sedmelluq.discord.lavaplayer.tools.io.ByteBufferInputStream; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream; import org.apache.http.HttpResponse; import javax.crypto.BadPaddingException; @@ -21,75 +22,75 @@ public class DeezerPersistentHttpStream extends PersistentHttpStream { - private final byte[] keyMaterial; + private final byte[] keyMaterial; - public DeezerPersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength, byte[] keyMaterial) { - super(httpInterface, contentUrl, contentLength); - this.keyMaterial = keyMaterial; - } + public DeezerPersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength, byte[] keyMaterial) { + super(httpInterface, contentUrl, contentLength); + this.keyMaterial = keyMaterial; + } - @Override - public InputStream createContentInputStream(HttpResponse response) throws IOException { - return new DecryptingInputStream(response.getEntity().getContent(), this.keyMaterial, this.position); - } + @Override + public InputStream createContentInputStream(HttpResponse response) throws IOException { + return new DecryptingInputStream(response.getEntity().getContent(), this.keyMaterial, this.position); + } - private static class DecryptingInputStream extends InputStream { + private static class DecryptingInputStream extends InputStream { - private static final int BLOCK_SIZE = 2048; - private static final byte[] iv = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; + private static final int BLOCK_SIZE = 2048; + private static final byte[] iv = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; - private final InputStream in; - private final ByteBuffer buff; - private final InputStream out; - private final Cipher cipher; - private long i; - private boolean filled; + private final InputStream in; + private final ByteBuffer buff; + private final InputStream out; + private final Cipher cipher; + private long i; + private boolean filled; - public DecryptingInputStream(InputStream in, byte[] keyMaterial, long position) throws IOException { - this.in = new BufferedInputStream(in); - this.buff = ByteBuffer.allocate(BLOCK_SIZE); - this.out = new ByteBufferInputStream(this.buff); + public DecryptingInputStream(InputStream in, byte[] keyMaterial, long position) throws IOException { + this.in = new BufferedInputStream(in); + this.buff = ByteBuffer.allocate(BLOCK_SIZE); + this.out = new ByteBufferInputStream(this.buff); - try { - cipher = Cipher.getInstance("Blowfish/CBC/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyMaterial, "Blowfish"), new IvParameterSpec(iv)); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | - InvalidAlgorithmParameterException e) { - throw new IOException(e); - } + try { + cipher = Cipher.getInstance("Blowfish/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyMaterial, "Blowfish"), new IvParameterSpec(iv)); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | + InvalidAlgorithmParameterException e) { + throw new IOException(e); + } - i = Math.max(0, position / BLOCK_SIZE); - var remainingBytesInChunk = ((i + 1) * BLOCK_SIZE) - position; - if (remainingBytesInChunk < 2048) { - in.skip(remainingBytesInChunk); - i++; - } - } + i = Math.max(0, position / BLOCK_SIZE); + var remainingBytesInChunk = ((i + 1) * BLOCK_SIZE) - position; + if (remainingBytesInChunk < 2048) { + in.skip(remainingBytesInChunk); + i++; + } + } - @Override - public int read() throws IOException { - if (this.filled && this.out.available() > 0) { - return this.out.read(); - } - var chunk = this.in.readNBytes(BLOCK_SIZE); - this.buff.clear(); - this.filled = true; - if (this.i % 3 > 0 || chunk.length < BLOCK_SIZE) { - this.buff.put(chunk); - } else { - byte[] decryptedChunk; - try { - decryptedChunk = this.cipher.doFinal(chunk); - } catch (IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException(e); - } - this.buff.put(decryptedChunk); - } - i++; - this.buff.flip(); - return this.out.read(); - } + @Override + public int read() throws IOException { + if (this.filled && this.out.available() > 0) { + return this.out.read(); + } + var chunk = this.in.readNBytes(BLOCK_SIZE); + this.buff.clear(); + this.filled = true; + if (this.i % 3 > 0 || chunk.length < BLOCK_SIZE) { + this.buff.put(chunk); + } else { + byte[] decryptedChunk; + try { + decryptedChunk = this.cipher.doFinal(chunk); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException(e); + } + this.buff.put(decryptedChunk); + } + i++; + this.buff.flip(); + return this.out.read(); + } - } + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java deleted file mode 100644 index 529156c3..00000000 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java +++ /dev/null @@ -1,312 +0,0 @@ -package com.github.topisenpai.lavasrc.deezer; - -import com.sedmelluq.discord.lavaplayer.tools.Units; -import com.sedmelluq.discord.lavaplayer.tools.io.EmptyInputStream; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; -import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoBuilder; -import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoProvider; -import org.apache.http.Header; -import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.Collections; -import java.util.List; - -import static com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools.getHeaderValue; -import static com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools.isSuccessWithContent; - -/** - * Use an HTTP endpoint as a stream, where the connection resetting is handled gracefully by reopening the connection - * and using a closed stream will just reopen the connection. - */ -public class PersistentHttpStream extends SeekableInputStream implements AutoCloseable { - - private static final Logger log = LoggerFactory.getLogger(com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream.class); - - private static final long MAX_SKIP_DISTANCE = 512L * 1024L; - protected final URI contentUrl; - private final HttpInterface httpInterface; - protected long position; - private int lastStatusCode; - private CloseableHttpResponse currentResponse; - private InputStream currentContent; - - /** - * @param httpInterface The HTTP interface to use for requests - * @param contentUrl The URL of the resource - * @param contentLength The length of the resource in bytes - */ - public PersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength) { - super(contentLength == null ? Units.CONTENT_LENGTH_UNKNOWN : contentLength, MAX_SKIP_DISTANCE); - - this.httpInterface = httpInterface; - this.contentUrl = contentUrl; - this.position = 0; - } - - private static boolean validateStatusCode(HttpResponse response, boolean returnOnServerError) { - int statusCode = response.getStatusLine().getStatusCode(); - if (returnOnServerError && statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR) { - return false; - } else if (!isSuccessWithContent(statusCode)) { - throw new RuntimeException("Not success status code: " + statusCode); - } - return true; - } - - /** - * Connect and return status code or return last status code if already connected. This causes the internal status - * code checker to be disabled, so non-success status codes will be returned instead of being thrown as they would - * be otherwise. - * - * @return The status code when connecting to the URL - * @throws IOException On IO error - */ - public int checkStatusCode() throws IOException { - connect(true); - - return lastStatusCode; - } - - /** - * @return An HTTP response if one is currently open. - */ - public HttpResponse getCurrentResponse() { - return currentResponse; - } - - protected URI getConnectUrl() { - return contentUrl; - } - - protected boolean useHeadersForRange() { - return true; - } - - private HttpGet getConnectRequest() { - HttpGet request = new HttpGet(getConnectUrl()); - - if (position > 0 && useHeadersForRange()) { - request.setHeader(HttpHeaders.RANGE, "bytes=" + position + "-"); - } - - return request; - } - - private void connect(boolean skipStatusCheck) throws IOException { - if (currentResponse == null) { - for (int i = 1; i >= 0; i--) { - if (attemptConnect(skipStatusCheck, i > 0)) { - break; - } - } - } - } - - public InputStream createContentInputStream(HttpResponse response) throws IOException { - return new BufferedInputStream(currentResponse.getEntity().getContent()); - } - - private boolean attemptConnect(boolean skipStatusCheck, boolean retryOnServerError) throws IOException { - currentResponse = httpInterface.execute(getConnectRequest()); - lastStatusCode = currentResponse.getStatusLine().getStatusCode(); - - if (!skipStatusCheck && !validateStatusCode(currentResponse, retryOnServerError)) { - return false; - } - - if (currentResponse.getEntity() == null) { - currentContent = EmptyInputStream.INSTANCE; - contentLength = 0; - return true; - } - - currentContent = createContentInputStream(currentResponse); - - if (contentLength == Units.CONTENT_LENGTH_UNKNOWN) { - Header header = currentResponse.getFirstHeader("Content-Length"); - - if (header != null) { - contentLength = Long.parseLong(header.getValue()); - } - } - - return true; - } - - private void handleNetworkException(IOException exception, boolean attemptReconnect) throws IOException { - if (!attemptReconnect || !HttpClientTools.isRetriableNetworkException(exception)) { - throw exception; - } - - close(); - - log.debug("Encountered retriable exception on url {}.", contentUrl, exception); - } - - private int internalRead(boolean attemptReconnect) throws IOException { - connect(false); - - try { - int result = currentContent.read(); - if (result >= 0) { - position++; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalRead(false); - } - } - - @Override - public int read() throws IOException { - return internalRead(true); - } - - private int internalRead(byte[] b, int off, int len, boolean attemptReconnect) throws IOException { - connect(false); - - try { - int result = currentContent.read(b, off, len); - if (result >= 0) { - position += result; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalRead(b, off, len, false); - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - return internalRead(b, off, len, true); - } - - private long internalSkip(long n, boolean attemptReconnect) throws IOException { - connect(false); - - try { - long result = currentContent.skip(n); - if (result >= 0) { - position += result; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalSkip(n, false); - } - } - - @Override - public long skip(long n) throws IOException { - return internalSkip(n, true); - } - - private int internalAvailable(boolean attemptReconnect) throws IOException { - connect(false); - - try { - return currentContent.available(); - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalAvailable(false); - } - } - - @Override - public int available() throws IOException { - return internalAvailable(true); - } - - @Override - public synchronized void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void close() throws IOException { - if (currentResponse != null) { - try { - currentResponse.close(); - } catch (IOException e) { - log.debug("Failed to close response.", e); - } - - currentResponse = null; - currentContent = null; - } - } - - /** - * Detach from the current connection, making sure not to close the connection when the stream is closed. - */ - public void releaseConnection() { - if (currentContent != null) { - try { - currentContent.close(); - } catch (IOException e) { - log.debug("Failed to close response stream.", e); - } - } - - currentResponse = null; - currentContent = null; - } - - @Override - public long getPosition() { - return position; - } - - @Override - protected void seekHard(long position) throws IOException { - close(); - - this.position = position; - } - - @Override - public boolean canSeekHard() { - return contentLength != Units.CONTENT_LENGTH_UNKNOWN; - } - - @Override - public List getTrackInfoProviders() { - if (currentResponse != null) { - return Collections.singletonList(createIceCastHeaderProvider()); - } else { - return Collections.emptyList(); - } - } - - private AudioTrackInfoProvider createIceCastHeaderProvider() { - AudioTrackInfoBuilder builder = AudioTrackInfoBuilder.empty() - .setTitle(getHeaderValue(currentResponse, "icy-description")) - .setAuthor(getHeaderValue(currentResponse, "icy-name")); - - if (builder.getTitle() == null) { - builder.setTitle(getHeaderValue(currentResponse, "icy-url")); - } - - return builder; - } - -} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java index 5c5ca6ff..ab90b4d2 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java @@ -10,39 +10,39 @@ public abstract class MirroringAudioSourceManager implements AudioSourceManager { - public static final String ISRC_PATTERN = "%ISRC%"; - public static final String QUERY_PATTERN = "%QUERY%"; - protected final AudioPlayerManager audioPlayerManager; - protected String[] providers = { - "ytsearch:\"" + ISRC_PATTERN + "\"", - "ytsearch:" + QUERY_PATTERN - }; - - protected MirroringAudioSourceManager(String[] providers, AudioPlayerManager audioPlayerManager) { - if (providers != null && providers.length > 0) { - this.providers = providers; - } - this.audioPlayerManager = audioPlayerManager; - } - - public String[] getProviders() { - return this.providers; - } - - public AudioPlayerManager getAudioPlayerManager() { - return this.audioPlayerManager; - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var isrcAudioTrack = ((MirroringAudioTrack) track); - DataFormatTools.writeNullableText(output, isrcAudioTrack.getISRC()); - DataFormatTools.writeNullableText(output, isrcAudioTrack.getArtworkURL()); - } + public static final String ISRC_PATTERN = "%ISRC%"; + public static final String QUERY_PATTERN = "%QUERY%"; + protected final AudioPlayerManager audioPlayerManager; + protected String[] providers = { + "ytsearch:\"" + ISRC_PATTERN + "\"", + "ytsearch:" + QUERY_PATTERN + }; + + protected MirroringAudioSourceManager(String[] providers, AudioPlayerManager audioPlayerManager) { + if (providers != null && providers.length > 0) { + this.providers = providers; + } + this.audioPlayerManager = audioPlayerManager; + } + + public String[] getProviders() { + return this.providers; + } + + public AudioPlayerManager getAudioPlayerManager() { + return this.audioPlayerManager; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var isrcAudioTrack = ((MirroringAudioTrack) track); + DataFormatTools.writeNullableText(output, isrcAudioTrack.getISRC()); + DataFormatTools.writeNullableText(output, isrcAudioTrack.getArtworkURL()); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java index 1731bfdf..0b76c1cb 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java @@ -23,110 +23,110 @@ public abstract class MirroringAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); - - protected final String isrc; - protected final String artworkURL; - protected final MirroringAudioSourceManager sourceManager; - - public MirroringAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, MirroringAudioSourceManager sourceManager) { - super(trackInfo); - this.isrc = isrc; - this.artworkURL = artworkURL; - this.sourceManager = sourceManager; - } - - public String getISRC() { - return this.isrc; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - private String getTrackTitle() { - var query = this.trackInfo.title; - if (!this.trackInfo.author.equals("unknown")) { - query += " " + this.trackInfo.author; - } - return query; - } - - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - AudioItem track = null; - - for (var provider : this.sourceManager.getProviders()) { - if (provider.startsWith(SpotifySourceManager.SEARCH_PREFIX)) { - log.warn("Can not use spotify search as search provider!"); - continue; - } - - if (provider.startsWith(AppleMusicSourceManager.SEARCH_PREFIX)) { - log.warn("Can not use apple music search as search provider!"); - continue; - } - - if (provider.contains(ISRC_PATTERN)) { - if (this.isrc != null) { - provider = provider.replace(ISRC_PATTERN, this.isrc); - } else { - log.debug("Ignoring identifier \"" + provider + "\" because this track does not have an ISRC!"); - continue; - } - } - - provider = provider.replace(QUERY_PATTERN, getTrackTitle()); - track = loadItem(provider); - if (track != AudioReference.NO_TRACK) { - break; - } - } - - if (track instanceof AudioPlaylist) { - track = ((AudioPlaylist) track).getTracks().get(0); - } - if (track instanceof InternalAudioTrack) { - processDelegate((InternalAudioTrack) track, executor); - return; - } - throw new FriendlyException("No matching track found", FriendlyException.Severity.COMMON, new TrackNotFoundException()); - } - - @Override - public AudioSourceManager getSourceManager() { - return this.sourceManager; - } - - private AudioItem loadItem(String query) { - var cf = new CompletableFuture(); - this.sourceManager.getAudioPlayerManager().loadItem(query, new AudioLoadResultHandler() { - - @Override - public void trackLoaded(AudioTrack track) { - log.debug("Track loaded: " + track.getIdentifier()); - cf.complete(track); - } - - @Override - public void playlistLoaded(AudioPlaylist playlist) { - log.debug("Playlist loaded: " + playlist.getName()); - cf.complete(playlist); - } - - @Override - public void noMatches() { - log.debug("No matches found for: " + query); - cf.complete(AudioReference.NO_TRACK); - } - - @Override - public void loadFailed(FriendlyException exception) { - log.debug("Failed to load: " + query); - cf.completeExceptionally(exception); - } - }); - return cf.join(); - } + private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); + + protected final String isrc; + protected final String artworkURL; + protected final MirroringAudioSourceManager sourceManager; + + public MirroringAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, MirroringAudioSourceManager sourceManager) { + super(trackInfo); + this.isrc = isrc; + this.artworkURL = artworkURL; + this.sourceManager = sourceManager; + } + + public String getISRC() { + return this.isrc; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + private String getTrackTitle() { + var query = this.trackInfo.title; + if (!this.trackInfo.author.equals("unknown")) { + query += " " + this.trackInfo.author; + } + return query; + } + + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + AudioItem track = null; + + for (var provider : this.sourceManager.getProviders()) { + if (provider.startsWith(SpotifySourceManager.SEARCH_PREFIX)) { + log.warn("Can not use spotify search as search provider!"); + continue; + } + + if (provider.startsWith(AppleMusicSourceManager.SEARCH_PREFIX)) { + log.warn("Can not use apple music search as search provider!"); + continue; + } + + if (provider.contains(ISRC_PATTERN)) { + if (this.isrc != null) { + provider = provider.replace(ISRC_PATTERN, this.isrc); + } else { + log.debug("Ignoring identifier \"" + provider + "\" because this track does not have an ISRC!"); + continue; + } + } + + provider = provider.replace(QUERY_PATTERN, getTrackTitle()); + track = loadItem(provider); + if (track != AudioReference.NO_TRACK) { + break; + } + } + + if (track instanceof AudioPlaylist) { + track = ((AudioPlaylist) track).getTracks().get(0); + } + if (track instanceof InternalAudioTrack) { + processDelegate((InternalAudioTrack) track, executor); + return; + } + throw new FriendlyException("No matching track found", FriendlyException.Severity.COMMON, new TrackNotFoundException()); + } + + @Override + public AudioSourceManager getSourceManager() { + return this.sourceManager; + } + + private AudioItem loadItem(String query) { + var cf = new CompletableFuture(); + this.sourceManager.getAudioPlayerManager().loadItem(query, new AudioLoadResultHandler() { + + @Override + public void trackLoaded(AudioTrack track) { + log.debug("Track loaded: " + track.getIdentifier()); + cf.complete(track); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) { + log.debug("Playlist loaded: " + playlist.getName()); + cf.complete(playlist); + } + + @Override + public void noMatches() { + log.debug("No matches found for: " + query); + cf.complete(AudioReference.NO_TRACK); + } + + @Override + public void loadFailed(FriendlyException exception) { + log.debug("Failed to load: " + query); + cf.completeExceptionally(exception); + } + }); + return cf.join(); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java index a2943fb6..52dcffa9 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java @@ -2,6 +2,6 @@ public class TrackNotFoundException extends RuntimeException { - private static final long serialVersionUID = 6550093849278285754L; + private static final long serialVersionUID = 6550093849278285754L; } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java new file mode 100644 index 00000000..39bc5091 --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.spotify; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class SpotifyAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public SpotifyAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java index 1f5ac271..db02863d 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java @@ -6,13 +6,13 @@ public class SpotifyAudioTrack extends MirroringAudioTrack { - public SpotifyAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, SpotifySourceManager sourceManager) { - super(trackInfo, isrc, artworkURL, sourceManager); - } + public SpotifyAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, SpotifySourceManager sourceManager) { + super(trackInfo, isrc, artworkURL, sourceManager); + } - @Override - protected AudioTrack makeShallowClone() { - return new SpotifyAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (SpotifySourceManager) this.sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new SpotifyAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (SpotifySourceManager) this.sourceManager); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java index 19dd8c65..6ab41cf9 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java @@ -35,256 +35,263 @@ public class SpotifySourceManager extends MirroringAudioSourceManager implements HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?open\\.spotify\\.com/(user/[a-zA-Z0-9-_]+/)?(?track|album|playlist|artist)/(?[a-zA-Z0-9-_]+)"); - public static final String SEARCH_PREFIX = "spsearch:"; - public static final String RECOMMENDATIONS_PREFIX = "sprec:"; - public static final int PLAYLIST_MAX_PAGE_ITEMS = 100; - public static final int ALBUM_MAX_PAGE_ITEMS = 50; - public static final String API_BASE = "https://api.spotify.com/v1/"; - private static final Logger log = LoggerFactory.getLogger(SpotifySourceManager.class); - - private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - private final String clientId; - private final String clientSecret; - private final String countryCode; - private String token; - private Instant tokenExpire; - - public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager) { - super(providers, audioPlayerManager); - - if (clientId == null || clientId.isEmpty()) { - throw new IllegalArgumentException("Spotify client id must be set"); - } - this.clientId = clientId; - - if (clientSecret == null || clientSecret.isEmpty()) { - throw new IllegalArgumentException("Spotify secret must be set"); - } - this.clientSecret = clientSecret; - - if (countryCode == null || countryCode.isEmpty()) { - countryCode = "US"; - } - this.countryCode = countryCode; - } - - @Override - public String getSourceName() { - return "spotify"; - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new SpotifyAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); - } - - if (reference.identifier.startsWith(RECOMMENDATIONS_PREFIX)) { - return this.getRecommendations(reference.identifier.substring(RECOMMENDATIONS_PREFIX.length()).trim()); - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (!matcher.find()) { - return null; - } - - var id = matcher.group("identifier"); - switch (matcher.group("type")) { - case "album": - return this.getAlbum(id); - - case "track": - return this.getTrack(id); - - case "playlist": - return this.getPlaylist(id); - - case "artist": - return this.getArtist(id); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public void requestToken() throws IOException { - var request = new HttpPost("https://accounts.spotify.com/api/token"); - request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8))); - request.setEntity(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("grant_type", "client_credentials")), StandardCharsets.UTF_8)); - - var json = HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - this.token = json.get("access_token").text(); - this.tokenExpire = Instant.now().plusSeconds(json.get("expires_in").asLong(0)); - } - - public String getToken() throws IOException { - if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { - this.requestToken(); - } - return this.token; - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.addHeader("Authorization", "Bearer " + this.getToken()); - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - public AudioItem getSearch(String query) throws IOException { - var json = this.getJson(API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track"); - if (json == null || json.get("tracks").get("items").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist("Search results for: " + query, this.parseTrackItems(json.get("tracks")), null, true); - } - - public AudioItem getRecommendations(String query) throws IOException { - var json = this.getJson(API_BASE + "recommendations?" + query); - if (json == null || json.get("tracks").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist("Spotify Recommendations:", this.parseTracks(json), null, false); - } - - public AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(API_BASE + "albums/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "albums/" + id + "/tracks?limit=" + ALBUM_MAX_PAGE_ITEMS + "&offset=" + offset); - offset += ALBUM_MAX_PAGE_ITEMS; - - tracks.addAll(this.parseTrackItems(page)); - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("name").text(), tracks, null, false); - - } - - public AudioItem getPlaylist(String id) throws IOException { - var json = this.getJson(API_BASE + "playlists/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "playlists/" + id + "/tracks?limit=" + PLAYLIST_MAX_PAGE_ITEMS + "&offset=" + offset); - offset += PLAYLIST_MAX_PAGE_ITEMS; - - for (var value : page.get("items").values()) { - var track = value.get("track"); - if (track.isNull() || track.get("is_local").asBoolean(false)) { - continue; - } - tracks.add(this.parseTrack(track)); - } - - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("name").text(), tracks, null, false); - - } - - public AudioItem getArtist(String id) throws IOException { - var json = this.getJson(API_BASE + "artists/" + id + "/top-tracks?market=" + this.countryCode); - if (json == null || json.get("tracks").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("tracks").index(0).get("artists").index(0).get("name").text() + "'s Top Tracks", this.parseTracks(json), null, false); - } - - public AudioItem getTrack(String id) throws IOException { - var json = this.getJson(API_BASE + "tracks/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - return parseTrack(json); - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var value : json.get("tracks").values()) { - tracks.add(this.parseTrack(value)); - } - return tracks; - } - - private List parseTrackItems(JsonBrowser json) { - var tracks = new ArrayList(); - for (var value : json.get("items").values()) { - if (value.get("is_local").asBoolean(false)) { - continue; - } - tracks.add(this.parseTrack(value)); - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - return new SpotifyAudioTrack( - new AudioTrackInfo( - json.get("name").text(), - json.get("artists").index(0).get("name").text(), - json.get("duration_ms").asLong(0), - json.get("id").text(), - false, - json.get("external_urls").get("spotify").text() - ), - json.get("external_ids").get("isrc").text(), - json.get("album").get("images").index(0).get("url").text(), - this - ); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?open\\.spotify\\.com/(user/[a-zA-Z0-9-_]+/)?(?track|album|playlist|artist)/(?[a-zA-Z0-9-_]+)"); + public static final String SEARCH_PREFIX = "spsearch:"; + public static final String RECOMMENDATIONS_PREFIX = "sprec:"; + public static final int PLAYLIST_MAX_PAGE_ITEMS = 100; + public static final int ALBUM_MAX_PAGE_ITEMS = 50; + public static final String API_BASE = "https://api.spotify.com/v1/"; + private static final Logger log = LoggerFactory.getLogger(SpotifySourceManager.class); + + private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + private final String clientId; + private final String clientSecret; + private final String countryCode; + private String token; + private Instant tokenExpire; + + public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager) { + super(providers, audioPlayerManager); + + if (clientId == null || clientId.isEmpty()) { + throw new IllegalArgumentException("Spotify client id must be set"); + } + this.clientId = clientId; + + if (clientSecret == null || clientSecret.isEmpty()) { + throw new IllegalArgumentException("Spotify secret must be set"); + } + this.clientSecret = clientSecret; + + if (countryCode == null || countryCode.isEmpty()) { + countryCode = "US"; + } + this.countryCode = countryCode; + } + + @Override + public String getSourceName() { + return "spotify"; + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new SpotifyAudioTrack(trackInfo, + DataFormatTools.readNullableText(input), + DataFormatTools.readNullableText(input), + this + ); + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); + } + + if (reference.identifier.startsWith(RECOMMENDATIONS_PREFIX)) { + return this.getRecommendations(reference.identifier.substring(RECOMMENDATIONS_PREFIX.length()).trim()); + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (!matcher.find()) { + return null; + } + + var id = matcher.group("identifier"); + switch (matcher.group("type")) { + case "album": + return this.getAlbum(id); + + case "track": + return this.getTrack(id); + + case "playlist": + return this.getPlaylist(id); + + case "artist": + return this.getArtist(id); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + public void requestToken() throws IOException { + var request = new HttpPost("https://accounts.spotify.com/api/token"); + request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8))); + request.setEntity(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("grant_type", "client_credentials")), StandardCharsets.UTF_8)); + + var json = HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + this.token = json.get("access_token").text(); + this.tokenExpire = Instant.now().plusSeconds(json.get("expires_in").asLong(0)); + } + + public String getToken() throws IOException { + if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { + this.requestToken(); + } + return this.token; + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.addHeader("Authorization", "Bearer " + this.getToken()); + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + public AudioItem getSearch(String query) throws IOException { + var json = this.getJson(API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track"); + if (json == null || json.get("tracks").get("items").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + return new BasicAudioPlaylist("Search results for: " + query, this.parseTrackItems(json.get("tracks")), null, true); + } + + public AudioItem getRecommendations(String query) throws IOException { + var json = this.getJson(API_BASE + "recommendations?" + query); + if (json == null || json.get("tracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + return new BasicAudioPlaylist("Spotify Recommendations:", this.parseTracks(json), null, false); + } + + public AudioItem getAlbum(String id) throws IOException { + var json = this.getJson(API_BASE + "albums/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "albums/" + id + "/tracks?limit=" + ALBUM_MAX_PAGE_ITEMS + "&offset=" + offset); + offset += ALBUM_MAX_PAGE_ITEMS; + + tracks.addAll(this.parseTrackItems(page)); + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("images").index(0).get("url").text(); + var author = json.get("author").get("name").text(); + return new SpotifyAudioPlaylist(json.get("name").text(), tracks, "album", id, artworkUrl, author); + + } + + public AudioItem getPlaylist(String id) throws IOException { + var json = this.getJson(API_BASE + "playlists/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "playlists/" + id + "/tracks?limit=" + PLAYLIST_MAX_PAGE_ITEMS + "&offset=" + offset); + offset += PLAYLIST_MAX_PAGE_ITEMS; + + for (var value : page.get("items").values()) { + var track = value.get("track"); + if (track.isNull() || track.get("is_local").asBoolean(false)) { + continue; + } + tracks.add(this.parseTrack(track)); + } + + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("images").index(0).get("url").text(); + var author = json.get("owner").get("string").text(); + return new SpotifyAudioPlaylist(json.get("name").text(), tracks, "playlist", id, artworkUrl, author); + + } + + public AudioItem getArtist(String id) throws IOException { + var json = this.getJson(API_BASE + "artists/" + id + "/top-tracks?market=" + this.countryCode); + if (json == null || json.get("tracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("tracks").index(0).get("album").get("images").index(0).get("url").text(); + var author = json.get("tracks").index(0).get("artists").index(0).get("name").text(); + return new SpotifyAudioPlaylist(author + "'s Top Tracks", this.parseTracks(json), "artist", id, artworkUrl, author); + } + + public AudioItem getTrack(String id) throws IOException { + var json = this.getJson(API_BASE + "tracks/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + return parseTrack(json); + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var value : json.get("tracks").values()) { + tracks.add(this.parseTrack(value)); + } + return tracks; + } + + private List parseTrackItems(JsonBrowser json) { + var tracks = new ArrayList(); + for (var value : json.get("items").values()) { + if (value.get("is_local").asBoolean(false)) { + continue; + } + tracks.add(this.parseTrack(value)); + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + return new SpotifyAudioTrack( + new AudioTrackInfo( + json.get("name").text(), + json.get("artists").index(0).get("name").text(), + json.get("duration_ms").asLong(0), + json.get("id").text(), + false, + json.get("external_urls").get("spotify").text() + ), + json.get("external_ids").get("isrc").text(), + json.get("album").get("images").index(0).get("url").text(), + this + ); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java new file mode 100644 index 00000000..c461018b --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.yandexmusic; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class YandexMusicAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public YandexMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java index 433ce6e6..a7ed0f6b 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java @@ -17,66 +17,66 @@ import java.security.NoSuchAlgorithmException; public class YandexMusicAudioTrack extends DelegatedAudioTrack { - private final String artworkURL; - private final YandexMusicSourceManager sourceManager; + private final String artworkURL; + private final YandexMusicSourceManager sourceManager; - public YandexMusicAudioTrack(AudioTrackInfo trackInfo, String artworkURL, YandexMusicSourceManager sourceManager) { - super(trackInfo); - this.artworkURL = artworkURL; - this.sourceManager = sourceManager; - } + public YandexMusicAudioTrack(AudioTrackInfo trackInfo, String artworkURL, YandexMusicSourceManager sourceManager) { + super(trackInfo); + this.artworkURL = artworkURL; + this.sourceManager = sourceManager; + } - public String getArtworkURL() { - return this.artworkURL; - } + public String getArtworkURL() { + return this.artworkURL; + } - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - var downloadLink = this.getDownloadURL(this.trackInfo.identifier); - try (var httpInterface = this.sourceManager.getHttpInterface()) { - try (var stream = new PersistentHttpStream(httpInterface, new URI(downloadLink), this.trackInfo.length)) { - processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); - } - } - } + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + var downloadLink = this.getDownloadURL(this.trackInfo.identifier); + try (var httpInterface = this.sourceManager.getHttpInterface()) { + try (var stream = new PersistentHttpStream(httpInterface, new URI(downloadLink), this.trackInfo.length)) { + processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); + } + } + } - @Override - protected AudioTrack makeShallowClone() { - return new YandexMusicAudioTrack(this.trackInfo, this.artworkURL, this.sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new YandexMusicAudioTrack(this.trackInfo, this.artworkURL, this.sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return this.sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return this.sourceManager; + } - private String getDownloadURL(String id) throws IOException, NoSuchAlgorithmException { - var json = this.sourceManager.getJson(YandexMusicSourceManager.PUBLIC_API_BASE + "/tracks/" + id + "/download-info"); - if (json.isNull() || json.get("result").values().isEmpty()) { - throw new IllegalStateException("No download URL found for track " + id); - } + private String getDownloadURL(String id) throws IOException, NoSuchAlgorithmException { + var json = this.sourceManager.getJson(YandexMusicSourceManager.PUBLIC_API_BASE + "/tracks/" + id + "/download-info"); + if (json.isNull() || json.get("result").values().isEmpty()) { + throw new IllegalStateException("No download URL found for track " + id); + } - var downloadInfoLink = json.get("result").values().get(0).get("downloadInfoUrl").text(); - var downloadInfo = this.sourceManager.getDownloadStrings(downloadInfoLink); - if (downloadInfo == null) { - throw new IllegalStateException("No download URL found for track " + id); - } + var downloadInfoLink = json.get("result").values().get(0).get("downloadInfoUrl").text(); + var downloadInfo = this.sourceManager.getDownloadStrings(downloadInfoLink); + if (downloadInfo == null) { + throw new IllegalStateException("No download URL found for track " + id); + } - var doc = Jsoup.parse(downloadInfo, "", Parser.xmlParser()); - var host = doc.select("host").text(); - var path = doc.select("path").text(); - var ts = doc.select("ts").text(); - var s = doc.select("s").text(); + var doc = Jsoup.parse(downloadInfo, "", Parser.xmlParser()); + var host = doc.select("host").text(); + var path = doc.select("path").text(); + var ts = doc.select("ts").text(); + var s = doc.select("s").text(); - var sign = "XGRlBW9FXlekgbPrRHuSiA" + path + s; - var md = MessageDigest.getInstance("MD5"); - var digest = md.digest(sign.getBytes(StandardCharsets.UTF_8)); - var sb = new StringBuilder(); - for (byte b : digest) { - sb.append(String.format("%02x", b)); - } - var md5 = sb.toString(); + var sign = "XGRlBW9FXlekgbPrRHuSiA" + path + s; + var md = MessageDigest.getInstance("MD5"); + var digest = md.digest(sign.getBytes(StandardCharsets.UTF_8)); + var sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + var md5 = sb.toString(); - return "https://" + host + "/get-mp3/" + md5 + "/" + ts + path; - } + return "https://" + host + "/get-mp3/" + md5 + "/" + ts + path; + } } \ No newline at end of file diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java index 4feae52f..71c63ab6 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java @@ -31,211 +31,221 @@ import java.util.regex.Pattern; public class YandexMusicSourceManager implements AudioSourceManager, HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/(?artist|album)/(?[0-9]+)/?((?track/)(?[0-9]+)/?)?"); - public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/users/(?[0-9A-Za-z@.-]+)/playlists/(?[0-9]+)/?"); - public static final String SEARCH_PREFIX = "ymsearch:"; - public static final String PUBLIC_API_BASE = "https://api.music.yandex.net"; - - private static final Logger log = LoggerFactory.getLogger(YandexMusicSourceManager.class); - - private final HttpInterfaceManager httpInterfaceManager; - - private final String accessToken; - - public YandexMusicSourceManager(String accessToken) { - if (accessToken == null || accessToken.isEmpty()) { - throw new IllegalArgumentException("Yandex Music accessToken must be set"); - } - this.accessToken = accessToken; - this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } - - @Override - public String getSourceName() { - return "yandexmusic"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (matcher.find()) { - switch (matcher.group("type1")) { - case "album": - if (matcher.group("type2") != null) { - var trackId = matcher.group("identifier2"); - return this.getTrack(trackId); - } - var albumId = matcher.group("identifier"); - return this.getAlbum(albumId); - case "artist": - var artistId = matcher.group("identifier"); - return this.getArtist(artistId); - } - return null; - } - matcher = URL_PLAYLIST_PATTERN.matcher(reference.identifier); - if (matcher.find()) { - var userId = matcher.group("identifier"); - var playlistId = matcher.group("identifier2"); - return this.getPlaylist(userId, playlistId); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - private AudioItem getSearch(String query) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/search?text=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track&page=0"); - if (json.isNull() || json.get("result").get("tracks").isNull()) { - return AudioReference.NO_TRACK; - } - var tracks = this.parseTracks(json.get("result").get("tracks").get("results")); - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist("Yandex Music Search: " + query, tracks, null, true); - } - - private AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/albums/" + id + "/with-tracks"); - if (json.isNull() || json.get("result").isNull()) { - return AudioReference.NO_TRACK; - } - var tracks = new ArrayList(); - for (var volume : json.get("result").get("volumes").values()) { - for (var track : volume.values()) { - tracks.add(this.parseTrack(track)); - } - } - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("result").get("title").text(), tracks, null, false); - } - - private AudioItem getTrack(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/tracks/" + id); - if (json.isNull() || json.get("result").values().get(0).get("available").text().equals("false")) { - return AudioReference.NO_TRACK; - } - return this.parseTrack(json.get("result").values().get(0)); - } - - private AudioItem getArtist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/artists/" + id + "/tracks?page-size=10"); - if (json.isNull() || json.get("result").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - var artistName = this.getJson(PUBLIC_API_BASE + "/artists/" + id).get("result").get("artist").get("name").text(); - var tracks = this.parseTracks(json.get("result").get("tracks")); - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(artistName + "'s Top Tracks", tracks, null, false); - } - - private AudioItem getPlaylist(String userString, String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/users/" + userString + "/playlists/" + id); - if (json.isNull() || json.get("result").isNull() || json.get("result").get("tracks").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - var tracks = new ArrayList(); - for (var track : json.get("result").get("tracks").values()) { - tracks.add(this.parseTrack(track.get("track"))); - } - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - var playlist_title = json.get("result").get("kind").text().equals("3") ? "Liked songs" : json.get("result").get("title").text(); - return new BasicAudioPlaylist(playlist_title, tracks, null, false); - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.setHeader("Accept", "application/json"); - request.setHeader("Authorization", "OAuth " + this.accessToken); - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - public String getDownloadStrings(String uri) throws IOException { - var request = new HttpGet(uri); - request.setHeader("Accept", "application/json"); - request.setHeader("Authorization", "OAuth " + this.accessToken); - return HttpClientTools.fetchResponseLines(this.httpInterfaceManager.getInterface(), request, "downloadinfo-xml-page")[0]; - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var track : json.values()) { - var parsedTrack = this.parseTrack(track); - if (parsedTrack != null) { - tracks.add(parsedTrack); - } - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - if (!json.get("available").asBoolean(false) || json.get("albums").values().isEmpty()) { - return null; - } - var id = json.get("id").text(); - var artist = json.get("major").get("name").text().equals("PODCASTS") ? json.get("albums").values().get(0).get("title").text() : json.get("artists").values().get(0).get("name").text(); - var coverUri = json.get("albums").values().get(0).get("coverUri").text(); - return new YandexMusicAudioTrack(new AudioTrackInfo( - json.get("title").text(), - artist, - json.get("durationMs").as(Long.class), - id, - false, - "https://music.yandex.ru/album/" + json.get("albums").values().get(0).get("id").text() + "/track/" + id), - coverUri != null ? "https://" + coverUri.replace("%%", "400x400") : null, - this - ); - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var yandexMusicAudioTrack = ((YandexMusicAudioTrack) track); - DataFormatTools.writeNullableText(output, yandexMusicAudioTrack.getArtworkURL()); - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new YandexMusicAudioTrack(trackInfo, DataFormatTools.readNullableText(input), this); - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - public HttpInterface getHttpInterface() { - return this.httpInterfaceManager.getInterface(); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/(?artist|album)/(?[0-9]+)/?((?track/)(?[0-9]+)/?)?"); + public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/users/(?[0-9A-Za-z@.-]+)/playlists/(?[0-9]+)/?"); + public static final String SEARCH_PREFIX = "ymsearch:"; + public static final String PUBLIC_API_BASE = "https://api.music.yandex.net"; + + private static final Logger log = LoggerFactory.getLogger(YandexMusicSourceManager.class); + + private final HttpInterfaceManager httpInterfaceManager; + + private final String accessToken; + + public YandexMusicSourceManager(String accessToken) { + if (accessToken == null || accessToken.isEmpty()) { + throw new IllegalArgumentException("Yandex Music accessToken must be set"); + } + this.accessToken = accessToken; + this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "yandexmusic"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (matcher.find()) { + switch (matcher.group("type1")) { + case "album": + if (matcher.group("type2") != null) { + var trackId = matcher.group("identifier2"); + return this.getTrack(trackId); + } + var albumId = matcher.group("identifier"); + return this.getAlbum(albumId); + case "artist": + var artistId = matcher.group("identifier"); + return this.getArtist(artistId); + } + return null; + } + matcher = URL_PLAYLIST_PATTERN.matcher(reference.identifier); + if (matcher.find()) { + var userId = matcher.group("identifier"); + var playlistId = matcher.group("identifier2"); + return this.getPlaylist(userId, playlistId); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + private AudioItem getSearch(String query) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/search?text=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track&page=0"); + if (json.isNull() || json.get("result").get("tracks").isNull()) { + return AudioReference.NO_TRACK; + } + var tracks = this.parseTracks(json.get("result").get("tracks").get("results")); + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + return new BasicAudioPlaylist("Yandex Music Search: " + query, tracks, null, true); + } + + private AudioItem getAlbum(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/albums/" + id + "/with-tracks"); + if (json.isNull() || json.get("result").isNull()) { + return AudioReference.NO_TRACK; + } + var tracks = new ArrayList(); + for (var volume : json.get("result").get("volumes").values()) { + for (var track : volume.values()) { + tracks.add(this.parseTrack(track)); + } + } + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var coverUri = json.get("result").get("coverUri").text(); + var author = json.get("result").get("artists").values().get(0).get("name").text(); + return new YandexMusicAudioPlaylist(json.get("result").get("title").text(), tracks, "album", id, this.formatCoverUri(coverUri), author); + } + + private AudioItem getTrack(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/tracks/" + id); + if (json.isNull() || json.get("result").values().get(0).get("available").text().equals("false")) { + return AudioReference.NO_TRACK; + } + return this.parseTrack(json.get("result").values().get(0)); + } + + private AudioItem getArtist(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/artists/" + id + "/tracks?page-size=10"); + if (json.isNull() || json.get("result").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + var tracks = this.parseTracks(json.get("result").get("tracks")); + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var artistJson = this.getJson(PUBLIC_API_BASE + "/artists/" + id); + var coverUri = json.get("result").get("coverUri").text(); + var author = artistJson.get("result").get("artist").get("name").text(); + return new YandexMusicAudioPlaylist(author + "'s Top Tracks", tracks, "artist", id, this.formatCoverUri(coverUri), author); + } + + private AudioItem getPlaylist(String userString, String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/users/" + userString + "/playlists/" + id); + if (json.isNull() || json.get("result").isNull() || json.get("result").get("tracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + var tracks = new ArrayList(); + for (var track : json.get("result").get("tracks").values()) { + tracks.add(this.parseTrack(track.get("track"))); + } + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var playlistTitle = json.get("result").get("kind").text().equals("3") ? "Liked songs" : json.get("result").get("title").text(); + var coverUri = json.get("result").get("cover").get("uri").text(); + var author = json.get("result").get("owner").get("name").text(); + return new YandexMusicAudioPlaylist(playlistTitle, tracks, "playlist", id, this.formatCoverUri(coverUri), author); + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.setHeader("Accept", "application/json"); + request.setHeader("Authorization", "OAuth " + this.accessToken); + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + public String getDownloadStrings(String uri) throws IOException { + var request = new HttpGet(uri); + request.setHeader("Accept", "application/json"); + request.setHeader("Authorization", "OAuth " + this.accessToken); + return HttpClientTools.fetchResponseLines(this.httpInterfaceManager.getInterface(), request, "downloadinfo-xml-page")[0]; + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var track : json.values()) { + var parsedTrack = this.parseTrack(track); + if (parsedTrack != null) { + tracks.add(parsedTrack); + } + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + if (!json.get("available").asBoolean(false) || json.get("albums").values().isEmpty()) { + return null; + } + var id = json.get("id").text(); + var artist = json.get("major").get("name").text().equals("PODCASTS") ? json.get("albums").values().get(0).get("title").text() : json.get("artists").values().get(0).get("name").text(); + var coverUri = json.get("coverUri").text(); + return new YandexMusicAudioTrack(new AudioTrackInfo( + json.get("title").text(), + artist, + json.get("durationMs").as(Long.class), + id, + false, + "https://music.yandex.ru/album/" + json.get("albums").values().get(0).get("id").text() + "/track/" + id), + this.formatCoverUri(coverUri), + this + ); + } + + private String formatCoverUri(String coverUri) { + return coverUri != null ? "https://" + coverUri.replace("%%", "400x400") : null; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var yandexMusicAudioTrack = ((YandexMusicAudioTrack) track); + DataFormatTools.writeNullableText(output, yandexMusicAudioTrack.getArtworkURL()); + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new YandexMusicAudioTrack(trackInfo, DataFormatTools.readNullableText(input), this); + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + public HttpInterface getHttpInterface() { + return this.httpInterfaceManager.getInterface(); + } } \ No newline at end of file diff --git a/plugin/build.gradle b/plugin/build.gradle index a05ac57d..fc5cd1e7 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -4,13 +4,8 @@ plugins { id "com.github.johnrengelman.shadow" version "7.1.1" } -var moduleName = "lavasrc-plugin" - -mainClassName = "org.springframework.boot.loader.JarLauncher" - dependencies { - compileOnly("dev.arbjerg.lavalink:plugin-api:3.6.1") - runtimeOnly("com.github.freyacodes.lavalink:Lavalink-Server:3.6.0") + compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:20b8030") implementation project(":main") } @@ -27,7 +22,7 @@ publishing { maven(MavenPublication) { pom { from components.java - artifactId moduleName + artifactId "lavasrc-plugin" } } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java index bc1e730e..c5076ebf 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java @@ -7,23 +7,23 @@ @Component public class AppleMusicConfig { - private String countryCode = "us"; - private String mediaAPIToken; + private String countryCode = "us"; + private String mediaAPIToken; - public String getCountryCode() { - return this.countryCode; - } + public String getCountryCode() { + return this.countryCode; + } - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } - public String getMediaAPIToken() { - return this.mediaAPIToken; - } + public String getMediaAPIToken() { + return this.mediaAPIToken; + } - public void setMediaAPIToken(String mediaAPIToken) { - this.mediaAPIToken = mediaAPIToken; - } + public void setMediaAPIToken(String mediaAPIToken) { + this.mediaAPIToken = mediaAPIToken; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java index 7258bbb5..d9f81f37 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java @@ -7,14 +7,14 @@ @Component public class DeezerConfig { - private String masterDecryptionKey; + private String masterDecryptionKey; - public String getMasterDecryptionKey() { - return this.masterDecryptionKey; - } + public String getMasterDecryptionKey() { + return this.masterDecryptionKey; + } - public void setMasterDecryptionKey(String masterDecryptionKey) { - this.masterDecryptionKey = masterDecryptionKey; - } + public void setMasterDecryptionKey(String masterDecryptionKey) { + this.masterDecryptionKey = masterDecryptionKey; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java index c5fc3da2..17ee83d8 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java @@ -10,17 +10,17 @@ @Component public class LavaSrcConfig { - private String[] providers = { - "ytsearch:\"" + ISRC_PATTERN + "\"", - "ytsearch:" + QUERY_PATTERN - }; + private String[] providers = { + "ytsearch:\"" + ISRC_PATTERN + "\"", + "ytsearch:" + QUERY_PATTERN + }; - public String[] getProviders() { - return this.providers; - } + public String[] getProviders() { + return this.providers; + } - public void setProviders(String[] providers) { - this.providers = providers; - } + public void setProviders(String[] providers) { + this.providers = providers; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java new file mode 100644 index 00000000..7d75a4bf --- /dev/null +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java @@ -0,0 +1,86 @@ +package com.github.topisenpai.lavasrc.plugin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioPlaylist; +import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioTrack; +import com.github.topisenpai.lavasrc.deezer.DeezerAudioPlaylist; +import com.github.topisenpai.lavasrc.deezer.DeezerAudioTrack; +import com.github.topisenpai.lavasrc.spotify.SpotifyAudioPlaylist; +import com.github.topisenpai.lavasrc.spotify.SpotifyAudioTrack; +import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioPlaylist; +import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import dev.arbjerg.lavalink.api.AudioPlaylistJsonAppender; +import dev.arbjerg.lavalink.api.AudioTrackJsonAppender; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class LavaSrcJsonAppender implements AudioTrackJsonAppender, AudioPlaylistJsonAppender { + + @Override + public Map appendFields(AudioPlaylist playlist) { + var map = new HashMap(); + + if (playlist instanceof SpotifyAudioPlaylist) { + var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; + map.put("type", new TextNode(spotifyPlaylist.getType())); + map.put("identifier", new TextNode(spotifyPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(spotifyPlaylist.getArtworkURL())); + map.put("author", new TextNode(spotifyPlaylist.getAuthor())); + + } else if (playlist instanceof AppleMusicAudioPlaylist) { + var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; + map.put("type", new TextNode(appleMusicPlaylist.getType())); + map.put("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(appleMusicPlaylist.getArtworkURL())); + map.put("author", new TextNode(appleMusicPlaylist.getAuthor())); + + } else if (playlist instanceof DeezerAudioPlaylist) { + var deezerPlaylist = (DeezerAudioPlaylist) playlist; + map.put("type", new TextNode(deezerPlaylist.getType())); + map.put("identifier", new TextNode(deezerPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(deezerPlaylist.getArtworkURL())); + map.put("author", new TextNode(deezerPlaylist.getAuthor())); + + } else if (playlist instanceof YandexMusicAudioPlaylist) { + var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; + map.put("type", new TextNode(yandexMusicPlaylist.getType())); + map.put("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(yandexMusicPlaylist.getArtworkURL())); + map.put("author", new TextNode(yandexMusicPlaylist.getAuthor())); + } + + return map; + } + + @Override + public Map appendFields(AudioTrack audioTrack) { + var map = new HashMap(); + + if (audioTrack instanceof SpotifyAudioTrack) { + var spotifyAudioTrack = (SpotifyAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(spotifyAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(spotifyAudioTrack.getISRC())); + + } else if (audioTrack instanceof AppleMusicAudioTrack) { + var appleMusicAudioTrack = (AppleMusicAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(appleMusicAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(appleMusicAudioTrack.getISRC())); + + } else if (audioTrack instanceof DeezerAudioTrack) { + var deezerAudioTrack = (DeezerAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(deezerAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(deezerAudioTrack.getISRC())); + } else if (audioTrack instanceof YandexMusicAudioTrack) { + var yandexMusicAudioTrack = (YandexMusicAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(yandexMusicAudioTrack.getArtworkURL())); + } + + return map; + } +} diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java index e2fbcd06..e26616cf 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java @@ -13,45 +13,45 @@ @Service public class LavaSrcPlugin implements AudioPlayerManagerConfiguration { - private static final Logger log = LoggerFactory.getLogger(LavaSrcPlugin.class); - - private final LavaSrcConfig pluginConfig; - private final SourcesConfig sourcesConfig; - private final SpotifyConfig spotifyConfig; - private final AppleMusicConfig appleMusicConfig; - private final YandexMusicConfig yandexMusicConfig; - - private final DeezerConfig deezerConfig; - - public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, SpotifyConfig spotifyConfig, AppleMusicConfig appleMusicConfig, DeezerConfig deezerConfig, YandexMusicConfig yandexMusicConfig) { - log.info("Loading LavaSrc plugin..."); - this.pluginConfig = pluginConfig; - this.sourcesConfig = sourcesConfig; - this.spotifyConfig = spotifyConfig; - this.appleMusicConfig = appleMusicConfig; - this.deezerConfig = deezerConfig; - this.yandexMusicConfig = yandexMusicConfig; - } - - @Override - public AudioPlayerManager configure(AudioPlayerManager manager) { - if (this.sourcesConfig.isSpotify()) { - log.info("Registering Spotify audio source manager..."); - manager.registerSourceManager(new SpotifySourceManager(this.pluginConfig.getProviders(), this.spotifyConfig.getClientId(), this.spotifyConfig.getClientSecret(), this.spotifyConfig.getCountryCode(), manager)); - } - if (this.sourcesConfig.isAppleMusic()) { - log.info("Registering Apple Music audio source manager..."); - manager.registerSourceManager(new AppleMusicSourceManager(this.pluginConfig.getProviders(), this.appleMusicConfig.getMediaAPIToken(), this.appleMusicConfig.getCountryCode(), manager)); - } - if (this.sourcesConfig.isDeezer()) { - log.info("Registering Deezer audio source manager..."); - manager.registerSourceManager(new DeezerAudioSourceManager(this.deezerConfig.getMasterDecryptionKey())); - } - if (this.sourcesConfig.isYandexMusic()) { - log.info("Registering Yandex Music audio source manager..."); - manager.registerSourceManager(new YandexMusicSourceManager(this.yandexMusicConfig.getAccessToken())); - } - return manager; - } + private static final Logger log = LoggerFactory.getLogger(LavaSrcPlugin.class); + + private final LavaSrcConfig pluginConfig; + private final SourcesConfig sourcesConfig; + private final SpotifyConfig spotifyConfig; + private final AppleMusicConfig appleMusicConfig; + private final YandexMusicConfig yandexMusicConfig; + + private final DeezerConfig deezerConfig; + + public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, SpotifyConfig spotifyConfig, AppleMusicConfig appleMusicConfig, DeezerConfig deezerConfig, YandexMusicConfig yandexMusicConfig) { + log.info("Loading LavaSrc plugin..."); + this.pluginConfig = pluginConfig; + this.sourcesConfig = sourcesConfig; + this.spotifyConfig = spotifyConfig; + this.appleMusicConfig = appleMusicConfig; + this.deezerConfig = deezerConfig; + this.yandexMusicConfig = yandexMusicConfig; + } + + @Override + public AudioPlayerManager configure(AudioPlayerManager manager) { + if (this.sourcesConfig.isSpotify()) { + log.info("Registering Spotify audio source manager..."); + manager.registerSourceManager(new SpotifySourceManager(this.pluginConfig.getProviders(), this.spotifyConfig.getClientId(), this.spotifyConfig.getClientSecret(), this.spotifyConfig.getCountryCode(), manager)); + } + if (this.sourcesConfig.isAppleMusic()) { + log.info("Registering Apple Music audio source manager..."); + manager.registerSourceManager(new AppleMusicSourceManager(this.pluginConfig.getProviders(), this.appleMusicConfig.getMediaAPIToken(), this.appleMusicConfig.getCountryCode(), manager)); + } + if (this.sourcesConfig.isDeezer()) { + log.info("Registering Deezer audio source manager..."); + manager.registerSourceManager(new DeezerAudioSourceManager(this.deezerConfig.getMasterDecryptionKey())); + } + if (this.sourcesConfig.isYandexMusic()) { + log.info("Registering Yandex Music audio source manager..."); + manager.registerSourceManager(new YandexMusicSourceManager(this.yandexMusicConfig.getAccessToken())); + } + return manager; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java index 45ea6bd1..2fcd7c28 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java @@ -8,40 +8,40 @@ @Component public class SourcesConfig { - private boolean spotify = false; - private boolean appleMusic = false; - private boolean deezer = false; - private boolean yandexMusic = false; - - public boolean isSpotify() { - return this.spotify; - } - - public void setSpotify(boolean spotify) { - this.spotify = spotify; - } - - public boolean isAppleMusic() { - return this.appleMusic; - } - - public void setAppleMusic(boolean appleMusic) { - this.appleMusic = appleMusic; - } - - public boolean isDeezer() { - return this.deezer; - } - - public void setDeezer(boolean deezer) { - this.deezer = deezer; - } - - public boolean isYandexMusic() { - return this.yandexMusic; - } - - public void setYandexMusic(boolean yandexMusic) { - this.yandexMusic = yandexMusic; - } + private boolean spotify = false; + private boolean appleMusic = false; + private boolean deezer = false; + private boolean yandexMusic = false; + + public boolean isSpotify() { + return this.spotify; + } + + public void setSpotify(boolean spotify) { + this.spotify = spotify; + } + + public boolean isAppleMusic() { + return this.appleMusic; + } + + public void setAppleMusic(boolean appleMusic) { + this.appleMusic = appleMusic; + } + + public boolean isDeezer() { + return this.deezer; + } + + public void setDeezer(boolean deezer) { + this.deezer = deezer; + } + + public boolean isYandexMusic() { + return this.yandexMusic; + } + + public void setYandexMusic(boolean yandexMusic) { + this.yandexMusic = yandexMusic; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java index dba46bf0..d345d063 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java @@ -7,32 +7,32 @@ @Component public class SpotifyConfig { - private String clientId; - private String clientSecret; - private String countryCode; + private String clientId; + private String clientSecret; + private String countryCode; - public String getClientId() { - return this.clientId; - } + public String getClientId() { + return this.clientId; + } - public void setClientId(String clientId) { - this.clientId = clientId; - } + public void setClientId(String clientId) { + this.clientId = clientId; + } - public String getClientSecret() { - return this.clientSecret; - } + public String getClientSecret() { + return this.clientSecret; + } - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } - public String getCountryCode() { - return this.countryCode; - } + public String getCountryCode() { + return this.countryCode; + } - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java index d4fa37be..dd873f68 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java @@ -7,13 +7,13 @@ @Component public class YandexMusicConfig { - private String accessToken; + private String accessToken; - public String getAccessToken() { - return this.accessToken; - } + public String getAccessToken() { + return this.accessToken; + } - public void setAccessToken(String accessToken) { - this.accessToken = accessToken; - } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } } diff --git a/plugin/src/main/resources/lavalink-plugins/lavasrc.properties b/plugin/src/main/resources/lavalink-plugins/lavasrc.properties index e6b2337b..0396e1e4 100644 --- a/plugin/src/main/resources/lavalink-plugins/lavasrc.properties +++ b/plugin/src/main/resources/lavalink-plugins/lavasrc.properties @@ -1,3 +1,3 @@ name=lavasrc path=com.github.topisenpai.lavasrc -version=3.1.6 +version=3.2.0 From 38e36b8ca6bc7cf164c67c1266a1b9f102fc0e31 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Mon, 28 Nov 2022 11:33:10 +0100 Subject: [PATCH 02/11] use new plugin data appender interface --- plugin/build.gradle | 9 ++- ...er.java => LavaSrcPluginDataAppender.java} | 58 +++++++++---------- 2 files changed, 36 insertions(+), 31 deletions(-) rename plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/{LavaSrcJsonAppender.java => LavaSrcPluginDataAppender.java} (91%) diff --git a/plugin/build.gradle b/plugin/build.gradle index fc5cd1e7..610b5884 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -4,8 +4,13 @@ plugins { id "com.github.johnrengelman.shadow" version "7.1.1" } +var moduleName = "lavasrc-plugin" + +mainClassName = "org.springframework.boot.loader.JarLauncher" + dependencies { - compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:20b8030") + compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:f66611f") + runtimeOnly("com.github.freyacodes.lavalink:Lavalink-Server:3.6.0") implementation project(":main") } @@ -22,7 +27,7 @@ publishing { maven(MavenPublication) { pom { from components.java - artifactId "lavasrc-plugin" + artifactId moduleName } } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java similarity index 91% rename from plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java rename to plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java index 7d75a4bf..23a6ae7d 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java @@ -12,18 +12,43 @@ import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import dev.arbjerg.lavalink.api.AudioPlaylistJsonAppender; -import dev.arbjerg.lavalink.api.AudioTrackJsonAppender; +import dev.arbjerg.lavalink.api.JsonPluginDataAppender; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component -public class LavaSrcJsonAppender implements AudioTrackJsonAppender, AudioPlaylistJsonAppender { +public class LavaSrcPluginDataAppender implements JsonPluginDataAppender { @Override - public Map appendFields(AudioPlaylist playlist) { + public Map addTrackPluginData(AudioTrack audioTrack) { + var map = new HashMap(); + + if (audioTrack instanceof SpotifyAudioTrack) { + var spotifyAudioTrack = (SpotifyAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(spotifyAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(spotifyAudioTrack.getISRC())); + + } else if (audioTrack instanceof AppleMusicAudioTrack) { + var appleMusicAudioTrack = (AppleMusicAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(appleMusicAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(appleMusicAudioTrack.getISRC())); + + } else if (audioTrack instanceof DeezerAudioTrack) { + var deezerAudioTrack = (DeezerAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(deezerAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(deezerAudioTrack.getISRC())); + } else if (audioTrack instanceof YandexMusicAudioTrack) { + var yandexMusicAudioTrack = (YandexMusicAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(yandexMusicAudioTrack.getArtworkURL())); + } + + return map; + } + + @Override + public Map addPlaylistPluginData(AudioPlaylist playlist) { var map = new HashMap(); if (playlist instanceof SpotifyAudioPlaylist) { @@ -58,29 +83,4 @@ public Map appendFields(AudioPlaylist playlist) { return map; } - @Override - public Map appendFields(AudioTrack audioTrack) { - var map = new HashMap(); - - if (audioTrack instanceof SpotifyAudioTrack) { - var spotifyAudioTrack = (SpotifyAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(spotifyAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(spotifyAudioTrack.getISRC())); - - } else if (audioTrack instanceof AppleMusicAudioTrack) { - var appleMusicAudioTrack = (AppleMusicAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(appleMusicAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(appleMusicAudioTrack.getISRC())); - - } else if (audioTrack instanceof DeezerAudioTrack) { - var deezerAudioTrack = (DeezerAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(deezerAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(deezerAudioTrack.getISRC())); - } else if (audioTrack instanceof YandexMusicAudioTrack) { - var yandexMusicAudioTrack = (YandexMusicAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(yandexMusicAudioTrack.getArtworkURL())); - } - - return map; - } } From 7e9226eb0e952bc9fc15aca756df9f256567bc4e Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Mon, 28 Nov 2022 03:38:59 +0100 Subject: [PATCH 03/11] implement audio track & playlist custom json fields --- build.gradle | 2 +- main/build.gradle | 2 +- .../applemusic/AppleMusicAudioPlaylist.java | 39 ++ .../applemusic/AppleMusicAudioTrack.java | 14 +- .../applemusic/AppleMusicSourceManager.java | 502 ++++++++--------- .../lavasrc/deezer/DeezerAudioPlaylist.java | 39 ++ .../deezer/DeezerAudioSourceManager.java | 425 ++++++++------- .../lavasrc/deezer/DeezerAudioTrack.java | 164 +++--- .../deezer/DeezerPersistentHttpStream.java | 121 ++--- .../lavasrc/deezer/PersistentHttpStream.java | 312 ----------- .../mirror/MirroringAudioSourceManager.java | 68 +-- .../lavasrc/mirror/MirroringAudioTrack.java | 210 ++++---- .../mirror/TrackNotFoundException.java | 2 +- .../lavasrc/spotify/SpotifyAudioPlaylist.java | 39 ++ .../lavasrc/spotify/SpotifyAudioTrack.java | 14 +- .../lavasrc/spotify/SpotifySourceManager.java | 509 +++++++++--------- .../yandexmusic/YandexMusicAudioPlaylist.java | 39 ++ .../yandexmusic/YandexMusicAudioTrack.java | 104 ++-- .../yandexmusic/YandexMusicSourceManager.java | 424 ++++++++------- plugin/build.gradle | 2 +- .../lavasrc/plugin/AppleMusicConfig.java | 28 +- .../lavasrc/plugin/DeezerConfig.java | 14 +- .../lavasrc/plugin/LavaSrcConfig.java | 20 +- .../lavasrc/plugin/LavaSrcJsonAppender.java | 86 +++ .../lavasrc/plugin/LavaSrcPlugin.java | 80 +-- .../lavasrc/plugin/SourcesConfig.java | 72 +-- .../lavasrc/plugin/SpotifyConfig.java | 42 +- .../lavasrc/plugin/YandexMusicConfig.java | 14 +- .../lavalink-plugins/lavasrc.properties | 2 +- 29 files changed, 1679 insertions(+), 1710 deletions(-) create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java delete mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java create mode 100644 plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java diff --git a/build.gradle b/build.gradle index 3c2e7167..31f6558a 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ allprojects { subprojects { apply plugin: "java" - version "3.1.6" + version "3.2.0" sourceCompatibility = 11 compileJava.options.encoding = "UTF-8" diff --git a/main/build.gradle b/main/build.gradle index 4daa414a..02f1eba1 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -6,7 +6,7 @@ plugins { var moduleName = "lavasrc" dependencies { - compileOnly "com.github.walkyst:lavaplayer-fork:1.3.98.4" + compileOnly "com.github.walkyst:lavaplayer-fork:1.3.99.1" implementation "org.jsoup:jsoup:1.14.3" implementation "commons-io:commons-io:2.6" } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java new file mode 100644 index 00000000..70166218 --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.applemusic; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class AppleMusicAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public AppleMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java index c27b2824..380ad7f4 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java @@ -6,13 +6,13 @@ public class AppleMusicAudioTrack extends MirroringAudioTrack { - public AppleMusicAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, AppleMusicSourceManager sourceManager) { - super(trackInfo, isrc, artworkURL, sourceManager); - } + public AppleMusicAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, AppleMusicSourceManager sourceManager) { + super(trackInfo, isrc, artworkURL, sourceManager); + } - @Override - protected AudioTrack makeShallowClone() { - return new AppleMusicAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (AppleMusicSourceManager) this.sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new AppleMusicAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (AppleMusicSourceManager) this.sourceManager); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java index 588a2bc9..011e5bcf 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java @@ -34,250 +34,262 @@ public class AppleMusicSourceManager extends MirroringAudioSourceManager implements HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?[a-zA-Z]{2}/)?(?album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?[a-zA-Z\\d\\-.]+)(\\?i=(?\\d+))?"); - public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?ey[\\w.-]+)\""); - public static final String SEARCH_PREFIX = "amsearch:"; - public static final int MAX_PAGE_ITEMS = 300; - public static final String API_BASE = "https://api.music.apple.com/v1/"; - private static final Logger log = LoggerFactory.getLogger(AppleMusicSourceManager.class); - private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - private final String countryCode; - private String token; - private String origin; - private Instant tokenExpire; - - public AppleMusicSourceManager(String[] providers, String mediaAPIToken, String countryCode, AudioPlayerManager audioPlayerManager) { - super(providers, audioPlayerManager); - this.token = mediaAPIToken; - try { - this.parseTokenData(); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot parse token for expire date and origin", e); - } - if (countryCode == null || countryCode.isEmpty()) { - this.countryCode = "us"; - } else { - this.countryCode = countryCode; - } - } - - @Override - public String getSourceName() { - return "applemusic"; - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new AppleMusicAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (!matcher.find()) { - return null; - } - - var countryCode = matcher.group("countrycode"); - var id = matcher.group("identifier"); - switch (matcher.group("type")) { - case "song": - return this.getSong(id, countryCode); - - case "album": - var id2 = matcher.group("identifier2"); - if (id2 == null || id2.isEmpty()) { - return this.getAlbum(id, countryCode); - } - return this.getSong(id2, countryCode); - - case "playlist": - return this.getPlaylist(id, countryCode); - - case "artist": - return this.getArtist(id, countryCode); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public void parseTokenData() throws IOException { - if (this.token == null || this.token.isEmpty()) { - return; - } - var json = JsonBrowser.parse(new String(Base64.getDecoder().decode(this.token.split("\\.")[1]))); - this.tokenExpire = Instant.ofEpochSecond(json.get("exp").asLong(0)); - this.origin = json.get("root_https_origin").index(0).text(); - } - - public void requestToken() throws IOException { - var request = new HttpGet("https://music.apple.com"); - String tokenScriptURL; - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - var document = Jsoup.parse(response.getEntity().getContent(), null, ""); - var element = document.selectFirst("script[type=module][src~=/assets/index.*.js]"); - if (element == null) { - throw new IllegalStateException("Cannot find token script element"); - } - tokenScriptURL = element.attr("src"); - } - if (tokenScriptURL.isEmpty()) { - throw new IllegalStateException("Cannot find token script url"); - } - request = new HttpGet("https://music.apple.com" + tokenScriptURL); - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - var tokenScript = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript); - if (!tokenMatcher.find()) { - throw new IllegalStateException("Cannot find token in script"); - } - this.token = tokenMatcher.group("token"); - } - this.parseTokenData(); - } - - public String getToken() throws IOException { - if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { - this.requestToken(); - } - return this.token; - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.addHeader("Authorization", "Bearer " + this.getToken()); - if (this.origin != null && !this.origin.isEmpty()) { - request.addHeader("Origin", "https://" + this.origin); - } - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - public AudioItem getSearch(String query) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/search?term=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&limit=" + 25); - if (json == null || json.get("results").get("songs").get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist("Apple Music Search: " + query, parseTracks(json.get("results").get("songs")), null, true); - } - - public AudioItem getAlbum(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); - offset += MAX_PAGE_ITEMS; - - tracks.addAll(parseTracks(page)); - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, null, false); - } - - public AudioItem getPlaylist(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); - offset += MAX_PAGE_ITEMS; - - tracks.addAll(parseTracks(page)); - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, null, false); - } - - public AudioItem getArtist(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/artists/" + id + "/view/top-songs"); - if (json == null || json.get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("artistName").text() + "'s Top Tracks", parseTracks(json), null, false); - } - - public AudioItem getSong(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/songs/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - return parseTrack(json.get("data").index(0)); - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var value : json.get("data").values()) { - tracks.add(this.parseTrack(value)); - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - var attributes = json.get("attributes"); - var artwork = attributes.get("artwork"); - return new AppleMusicAudioTrack( - new AudioTrackInfo( - attributes.get("name").text(), - attributes.get("artistName").text(), - attributes.get("durationInMillis").asLong(0), - json.get("id").text(), - false, - attributes.get("url").text() - ), - attributes.get("isrc").text(), - artwork.get("url").text().replace("{w}", artwork.get("width").text()).replace("{h}", artwork.get("height").text()), - this - ); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?[a-zA-Z]{2}/)?(?album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?[a-zA-Z\\d\\-.]+)(\\?i=(?\\d+))?"); + public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?ey[\\w.-]+)\""); + public static final String SEARCH_PREFIX = "amsearch:"; + public static final int MAX_PAGE_ITEMS = 300; + public static final String API_BASE = "https://api.music.apple.com/v1/"; + private static final Logger log = LoggerFactory.getLogger(AppleMusicSourceManager.class); + private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + private final String countryCode; + private String token; + private String origin; + private Instant tokenExpire; + + public AppleMusicSourceManager(String[] providers, String mediaAPIToken, String countryCode, AudioPlayerManager audioPlayerManager) { + super(providers, audioPlayerManager); + this.token = mediaAPIToken; + try { + this.parseTokenData(); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot parse token for expire date and origin", e); + } + if (countryCode == null || countryCode.isEmpty()) { + this.countryCode = "us"; + } else { + this.countryCode = countryCode; + } + } + + @Override + public String getSourceName() { + return "applemusic"; + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new AppleMusicAudioTrack(trackInfo, + DataFormatTools.readNullableText(input), + DataFormatTools.readNullableText(input), + this + ); + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (!matcher.find()) { + return null; + } + + var countryCode = matcher.group("countrycode"); + var id = matcher.group("identifier"); + switch (matcher.group("type")) { + case "song": + return this.getSong(id, countryCode); + + case "album": + var id2 = matcher.group("identifier2"); + if (id2 == null || id2.isEmpty()) { + return this.getAlbum(id, countryCode); + } + return this.getSong(id2, countryCode); + + case "playlist": + return this.getPlaylist(id, countryCode); + + case "artist": + return this.getArtist(id, countryCode); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + public void parseTokenData() throws IOException { + if (this.token == null || this.token.isEmpty()) { + return; + } + var json = JsonBrowser.parse(new String(Base64.getDecoder().decode(this.token.split("\\.")[1]))); + this.tokenExpire = Instant.ofEpochSecond(json.get("exp").asLong(0)); + this.origin = json.get("root_https_origin").index(0).text(); + } + + public void requestToken() throws IOException { + var request = new HttpGet("https://music.apple.com"); + String tokenScriptURL; + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + var document = Jsoup.parse(response.getEntity().getContent(), null, ""); + var element = document.selectFirst("script[type=module][src~=/assets/index.*.js]"); + if (element == null) { + throw new IllegalStateException("Cannot find token script element"); + } + tokenScriptURL = element.attr("src"); + } + if (tokenScriptURL.isEmpty()) { + throw new IllegalStateException("Cannot find token script url"); + } + request = new HttpGet("https://music.apple.com" + tokenScriptURL); + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + var tokenScript = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript); + if (!tokenMatcher.find()) { + throw new IllegalStateException("Cannot find token in script"); + } + this.token = tokenMatcher.group("token"); + } + this.parseTokenData(); + } + + public String getToken() throws IOException { + if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { + this.requestToken(); + } + return this.token; + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.addHeader("Authorization", "Bearer " + this.getToken()); + if (this.origin != null && !this.origin.isEmpty()) { + request.addHeader("Origin", "https://" + this.origin); + } + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + public AudioItem getSearch(String query) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/search?term=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&limit=" + 25); + if (json == null || json.get("results").get("songs").get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + return new BasicAudioPlaylist("Apple Music Search: " + query, parseTracks(json.get("results").get("songs")), null, true); + } + + public AudioItem getAlbum(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); + offset += MAX_PAGE_ITEMS; + + tracks.addAll(parseTracks(page)); + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = this.parseArtworkUrl(json.get("data").index(0).get("attributes").get("artwork")); + var author = json.get("data").index(0).get("attributes").get("artistName").text(); + return new AppleMusicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, "album", id, artworkUrl, author); + } + + public AudioItem getPlaylist(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); + offset += MAX_PAGE_ITEMS; + + tracks.addAll(parseTracks(page)); + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = this.parseArtworkUrl(json.get("data").index(0).get("attributes").get("artwork")); + var author = json.get("data").index(0).get("attributes").get("curatorName").text(); + return new AppleMusicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, "playlist", id, artworkUrl, author); + } + + public AudioItem getArtist(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/artists/" + id + "/view/top-songs"); + if (json == null || json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var jsonArtist = this.getJson(API_BASE + "catalog/" + countryCode + "/artists/" + id); + + var artworkUrl = this.parseArtworkUrl(jsonArtist.get("data").index(0).get("attributes").get("artwork")); + var author = jsonArtist.get("data").index(0).get("attributes").get("name").text(); + return new AppleMusicAudioPlaylist(author + "'s Top Tracks", parseTracks(json), "artist", id, artworkUrl, author); + } + + public AudioItem getSong(String id, String countryCode) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/songs/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + return parseTrack(json.get("data").index(0)); + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var value : json.get("data").values()) { + tracks.add(this.parseTrack(value)); + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + var attributes = json.get("attributes"); + return new AppleMusicAudioTrack( + new AudioTrackInfo( + attributes.get("name").text(), + attributes.get("artistName").text(), + attributes.get("durationInMillis").asLong(0), + json.get("id").text(), + false, + attributes.get("url").text() + ), + attributes.get("isrc").text(), + this.parseArtworkUrl(attributes.get("artwork")), + this + ); + } + + private String parseArtworkUrl(JsonBrowser json) { + return json.get("url").text().replace("{w}", json.get("width").text()).replace("{h}", json.get("height").text()); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java new file mode 100644 index 00000000..901a448e --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.deezer; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class DeezerAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public DeezerAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java index 350cb157..85777bf9 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java @@ -32,213 +32,222 @@ public class DeezerAudioSourceManager implements AudioSourceManager, HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?deezer\\.com/(?[a-zA-Z]{2}/)?(?track|album|playlist|artist)/(?[0-9]+)"); - public static final String SEARCH_PREFIX = "dzsearch:"; - public static final String ISRC_PREFIX = "dzisrc:"; - public static final String SHARE_URL = "https://deezer.page.link/"; - public static final String PUBLIC_API_BASE = "https://api.deezer.com/2.0"; - public static final String PRIVATE_API_BASE = "https://www.deezer.com/ajax/gw-light.php"; - public static final String MEDIA_BASE = "https://media.deezer.com/v1"; - - private static final Logger log = LoggerFactory.getLogger(DeezerAudioSourceManager.class); - - private final String masterDecryptionKey; - - private final HttpInterfaceManager httpInterfaceManager; - - public DeezerAudioSourceManager(String masterDecryptionKey) { - if (masterDecryptionKey == null || masterDecryptionKey.isEmpty()) { - throw new IllegalArgumentException("Deezer master key must be set"); - } - this.masterDecryptionKey = masterDecryptionKey; - this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } - - @Override - public String getSourceName() { - return "deezer"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); - } - - if (reference.identifier.startsWith(ISRC_PREFIX)) { - return this.getTrackByISRC(reference.identifier.substring(ISRC_PREFIX.length())); - } - - // If the identifier is a share URL, we need to follow the redirect to find out the real url behind it - if (reference.identifier.startsWith(SHARE_URL)) { - var request = new HttpGet(reference.identifier); - request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - if (response.getStatusLine().getStatusCode() == 302) { - var location = response.getFirstHeader("Location").getValue(); - if (location.startsWith("https://www.deezer.com/")) { - return this.loadItem(manager, new AudioReference(location, reference.title)); - } - } - return null; - } - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (!matcher.find()) { - return null; - } - - var id = matcher.group("identifier"); - switch (matcher.group("type")) { - case "album": - return this.getAlbum(id); - - case "track": - return this.getTrack(id); - - case "playlist": - return this.getPlaylist(id); - - case "artist": - return this.getArtist(id); - } - - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.setHeader("Accept", "application/json"); - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var track : json.get("data").values()) { - if (!track.get("type").text().equals("track")) { - continue; - } - tracks.add(this.parseTrack(track)); - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - var id = json.get("id").text(); - return new DeezerAudioTrack(new AudioTrackInfo( - json.get("title").text(), - json.get("artist").get("name").text(), - json.get("duration").as(Long.class) * 1000, - id, - false, - "https://deezer.com/track/" + id), - json.get("isrc").text(), - json.get("album").get("cover_xl").text(), - this - ); - } - - private AudioItem getTrackByISRC(String isrc) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/track/isrc:" + isrc); - if (json == null || json.get("id").isNull()) { - return AudioReference.NO_TRACK; - } - return this.parseTrack(json); - } - - private AudioItem getSearch(String query) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8)); - if (json == null || json.get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - - var tracks = this.parseTracks(json); - return new BasicAudioPlaylist("Deezer Search: " + query, tracks, null, true); - } - - private AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/album/" + id); - if (json == null || json.get("tracks").get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), null, false); - } - - private AudioItem getTrack(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/track/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - return this.parseTrack(json); - } - - private AudioItem getPlaylist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/playlist/" + id); - if (json == null || json.get("tracks").get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), null, false); - } - - private AudioItem getArtist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/artist/" + id + "/top?limit=50"); - if (json == null || json.get("data").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("data").index(0).get("artist").get("name").text() + "'s Top Tracks", this.parseTracks(json), null, false); - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var deezerAudioTrack = ((DeezerAudioTrack) track); - DataFormatTools.writeNullableText(output, deezerAudioTrack.getISRC()); - DataFormatTools.writeNullableText(output, deezerAudioTrack.getArtworkURL()); - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new DeezerAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } - - public String getMasterDecryptionKey() { - return this.masterDecryptionKey; - } - - public HttpInterface getHttpInterface() { - return this.httpInterfaceManager.getInterface(); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?deezer\\.com/(?[a-zA-Z]{2}/)?(?track|album|playlist|artist)/(?[0-9]+)"); + public static final String SEARCH_PREFIX = "dzsearch:"; + public static final String ISRC_PREFIX = "dzisrc:"; + public static final String SHARE_URL = "https://deezer.page.link/"; + public static final String PUBLIC_API_BASE = "https://api.deezer.com/2.0"; + public static final String PRIVATE_API_BASE = "https://www.deezer.com/ajax/gw-light.php"; + public static final String MEDIA_BASE = "https://media.deezer.com/v1"; + + private static final Logger log = LoggerFactory.getLogger(DeezerAudioSourceManager.class); + + private final String masterDecryptionKey; + + private final HttpInterfaceManager httpInterfaceManager; + + public DeezerAudioSourceManager(String masterDecryptionKey) { + if (masterDecryptionKey == null || masterDecryptionKey.isEmpty()) { + throw new IllegalArgumentException("Deezer master key must be set"); + } + this.masterDecryptionKey = masterDecryptionKey; + this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "deezer"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); + } + + if (reference.identifier.startsWith(ISRC_PREFIX)) { + return this.getTrackByISRC(reference.identifier.substring(ISRC_PREFIX.length())); + } + + // If the identifier is a share URL, we need to follow the redirect to find out the real url behind it + if (reference.identifier.startsWith(SHARE_URL)) { + var request = new HttpGet(reference.identifier); + request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + if (response.getStatusLine().getStatusCode() == 302) { + var location = response.getFirstHeader("Location").getValue(); + if (location.startsWith("https://www.deezer.com/")) { + return this.loadItem(manager, new AudioReference(location, reference.title)); + } + } + return null; + } + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (!matcher.find()) { + return null; + } + + var id = matcher.group("identifier"); + switch (matcher.group("type")) { + case "album": + return this.getAlbum(id); + + case "track": + return this.getTrack(id); + + case "playlist": + return this.getPlaylist(id); + + case "artist": + return this.getArtist(id); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.setHeader("Accept", "application/json"); + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var track : json.get("data").values()) { + if (!track.get("type").text().equals("track")) { + continue; + } + tracks.add(this.parseTrack(track)); + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + var id = json.get("id").text(); + return new DeezerAudioTrack(new AudioTrackInfo( + json.get("title").text(), + json.get("artist").get("name").text(), + json.get("duration").as(Long.class) * 1000, + id, + false, + "https://deezer.com/track/" + id), + json.get("isrc").text(), + json.get("album").get("cover_xl").text(), + this + ); + } + + private AudioItem getTrackByISRC(String isrc) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/track/isrc:" + isrc); + if (json == null || json.get("id").isNull()) { + return AudioReference.NO_TRACK; + } + return this.parseTrack(json); + } + + private AudioItem getSearch(String query) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8)); + if (json == null || json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var tracks = this.parseTracks(json); + return new BasicAudioPlaylist("Deezer Search: " + query, tracks, null, true); + } + + private AudioItem getAlbum(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/album/" + id); + if (json == null || json.get("tracks").get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("cover_xl").text(); + var author = json.get("contributors").values().get(0).get("name").text(); + return new DeezerAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), "album", id, artworkUrl, author); + } + + private AudioItem getTrack(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/track/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + return this.parseTrack(json); + } + + private AudioItem getPlaylist(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/playlist/" + id); + if (json == null || json.get("tracks").get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("picture_xl").text(); + var author = json.get("creator").get("name").text(); + return new DeezerAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks")), "playlist", id, artworkUrl, author); + } + + private AudioItem getArtist(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/artist/" + id + "/top?limit=50"); + if (json == null || json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("data").index(0).get("contributors").get("picture_xl").text(); + var author = json.get("data").index(0).get("contributors").get("name").text(); + return new DeezerAudioPlaylist(author + "'s Top Tracks", this.parseTracks(json), "artist", id, artworkUrl, author); + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var deezerAudioTrack = ((DeezerAudioTrack) track); + DataFormatTools.writeNullableText(output, deezerAudioTrack.getISRC()); + DataFormatTools.writeNullableText(output, deezerAudioTrack.getArtworkURL()); + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new DeezerAudioTrack(trackInfo, + DataFormatTools.readNullableText(input), + DataFormatTools.readNullableText(input), + this + ); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } + + public String getMasterDecryptionKey() { + return this.masterDecryptionKey; + } + + public HttpInterface getHttpInterface() { + return this.httpInterfaceManager.getInterface(); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java index 5f986a39..3402815e 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java @@ -20,87 +20,87 @@ public class DeezerAudioTrack extends DelegatedAudioTrack { - private final String isrc; - private final String artworkURL; - private final DeezerAudioSourceManager sourceManager; - - public DeezerAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, DeezerAudioSourceManager sourceManager) { - super(trackInfo); - this.isrc = isrc; - this.artworkURL = artworkURL; - this.sourceManager = sourceManager; - } - - public String getISRC() { - return this.isrc; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getSessionID); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("Failed to get session ID"); - } - var sessionID = json.get("results").get("SESSION").text(); - - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getUserToken); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("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(); - - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getTrackToken); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("Failed to get track token"); - } - var trackToken = json.get("results").get("TRACK_TOKEN").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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getMediaURL); - if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { - throw new IllegalStateException("Failed to get media URL"); - } - return new URI(json.get("data").index(0).get("media").index(0).get("sources").index(0).get("url").text()); - } - - private byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException { - var md5 = Hex.encodeHex(MessageDigest.getInstance("MD5").digest(this.trackInfo.identifier.getBytes()), true); - var master_key = this.sourceManager.getMasterDecryptionKey().getBytes(); - - var key = new byte[16]; - for (int i = 0; i < 16; i++) { - key[i] = (byte) (md5[i] ^ md5[i + 16] ^ master_key[i]); - } - return key; - } - - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - try (var httpInterface = this.sourceManager.getHttpInterface()) { - try (var stream = new DeezerPersistentHttpStream(httpInterface, this.getTrackMediaURI(), this.trackInfo.length, this.getTrackDecryptionKey())) { - processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); - } - } - } - - @Override - protected AudioTrack makeShallowClone() { - return new DeezerAudioTrack(this.trackInfo, this.isrc, this.artworkURL, this.sourceManager); - } - - @Override - public AudioSourceManager getSourceManager() { - return this.sourceManager; - } + private final String isrc; + private final String artworkURL; + private final DeezerAudioSourceManager sourceManager; + + public DeezerAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, DeezerAudioSourceManager sourceManager) { + super(trackInfo); + this.isrc = isrc; + this.artworkURL = artworkURL; + this.sourceManager = sourceManager; + } + + public String getISRC() { + return this.isrc; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getSessionID); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("Failed to get session ID"); + } + var sessionID = json.get("results").get("SESSION").text(); + + 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getUserToken); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("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(); + + 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getTrackToken); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("Failed to get track token"); + } + var trackToken = json.get("results").get("TRACK_TOKEN").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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getMediaURL); + if (json.get("data").index(0).get("errors").index(0).get("code").asLong(0) != 0) { + throw new IllegalStateException("Failed to get media URL"); + } + return new URI(json.get("data").index(0).get("media").index(0).get("sources").index(0).get("url").text()); + } + + private byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException { + var md5 = Hex.encodeHex(MessageDigest.getInstance("MD5").digest(this.trackInfo.identifier.getBytes()), true); + var master_key = this.sourceManager.getMasterDecryptionKey().getBytes(); + + var key = new byte[16]; + for (int i = 0; i < 16; i++) { + key[i] = (byte) (md5[i] ^ md5[i + 16] ^ master_key[i]); + } + return key; + } + + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + try (var httpInterface = this.sourceManager.getHttpInterface()) { + try (var stream = new DeezerPersistentHttpStream(httpInterface, this.getTrackMediaURI(), this.trackInfo.length, this.getTrackDecryptionKey())) { + processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); + } + } + } + + @Override + protected AudioTrack makeShallowClone() { + return new DeezerAudioTrack(this.trackInfo, this.isrc, this.artworkURL, this.sourceManager); + } + + @Override + public AudioSourceManager getSourceManager() { + return this.sourceManager; + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java index 500e0b7c..5e9e7197 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java @@ -2,6 +2,7 @@ import com.sedmelluq.discord.lavaplayer.tools.io.ByteBufferInputStream; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream; import org.apache.http.HttpResponse; import javax.crypto.BadPaddingException; @@ -21,75 +22,75 @@ public class DeezerPersistentHttpStream extends PersistentHttpStream { - private final byte[] keyMaterial; + private final byte[] keyMaterial; - public DeezerPersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength, byte[] keyMaterial) { - super(httpInterface, contentUrl, contentLength); - this.keyMaterial = keyMaterial; - } + public DeezerPersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength, byte[] keyMaterial) { + super(httpInterface, contentUrl, contentLength); + this.keyMaterial = keyMaterial; + } - @Override - public InputStream createContentInputStream(HttpResponse response) throws IOException { - return new DecryptingInputStream(response.getEntity().getContent(), this.keyMaterial, this.position); - } + @Override + public InputStream createContentInputStream(HttpResponse response) throws IOException { + return new DecryptingInputStream(response.getEntity().getContent(), this.keyMaterial, this.position); + } - private static class DecryptingInputStream extends InputStream { + private static class DecryptingInputStream extends InputStream { - private static final int BLOCK_SIZE = 2048; - private static final byte[] iv = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; + private static final int BLOCK_SIZE = 2048; + private static final byte[] iv = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; - private final InputStream in; - private final ByteBuffer buff; - private final InputStream out; - private final Cipher cipher; - private long i; - private boolean filled; + private final InputStream in; + private final ByteBuffer buff; + private final InputStream out; + private final Cipher cipher; + private long i; + private boolean filled; - public DecryptingInputStream(InputStream in, byte[] keyMaterial, long position) throws IOException { - this.in = new BufferedInputStream(in); - this.buff = ByteBuffer.allocate(BLOCK_SIZE); - this.out = new ByteBufferInputStream(this.buff); + public DecryptingInputStream(InputStream in, byte[] keyMaterial, long position) throws IOException { + this.in = new BufferedInputStream(in); + this.buff = ByteBuffer.allocate(BLOCK_SIZE); + this.out = new ByteBufferInputStream(this.buff); - try { - cipher = Cipher.getInstance("Blowfish/CBC/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyMaterial, "Blowfish"), new IvParameterSpec(iv)); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | - InvalidAlgorithmParameterException e) { - throw new IOException(e); - } + try { + cipher = Cipher.getInstance("Blowfish/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyMaterial, "Blowfish"), new IvParameterSpec(iv)); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | + InvalidAlgorithmParameterException e) { + throw new IOException(e); + } - i = Math.max(0, position / BLOCK_SIZE); - var remainingBytesInChunk = ((i + 1) * BLOCK_SIZE) - position; - if (remainingBytesInChunk < 2048) { - in.skip(remainingBytesInChunk); - i++; - } - } + i = Math.max(0, position / BLOCK_SIZE); + var remainingBytesInChunk = ((i + 1) * BLOCK_SIZE) - position; + if (remainingBytesInChunk < 2048) { + in.skip(remainingBytesInChunk); + i++; + } + } - @Override - public int read() throws IOException { - if (this.filled && this.out.available() > 0) { - return this.out.read(); - } - var chunk = this.in.readNBytes(BLOCK_SIZE); - this.buff.clear(); - this.filled = true; - if (this.i % 3 > 0 || chunk.length < BLOCK_SIZE) { - this.buff.put(chunk); - } else { - byte[] decryptedChunk; - try { - decryptedChunk = this.cipher.doFinal(chunk); - } catch (IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException(e); - } - this.buff.put(decryptedChunk); - } - i++; - this.buff.flip(); - return this.out.read(); - } + @Override + public int read() throws IOException { + if (this.filled && this.out.available() > 0) { + return this.out.read(); + } + var chunk = this.in.readNBytes(BLOCK_SIZE); + this.buff.clear(); + this.filled = true; + if (this.i % 3 > 0 || chunk.length < BLOCK_SIZE) { + this.buff.put(chunk); + } else { + byte[] decryptedChunk; + try { + decryptedChunk = this.cipher.doFinal(chunk); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException(e); + } + this.buff.put(decryptedChunk); + } + i++; + this.buff.flip(); + return this.out.read(); + } - } + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java deleted file mode 100644 index 529156c3..00000000 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/PersistentHttpStream.java +++ /dev/null @@ -1,312 +0,0 @@ -package com.github.topisenpai.lavasrc.deezer; - -import com.sedmelluq.discord.lavaplayer.tools.Units; -import com.sedmelluq.discord.lavaplayer.tools.io.EmptyInputStream; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; -import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoBuilder; -import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoProvider; -import org.apache.http.Header; -import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.Collections; -import java.util.List; - -import static com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools.getHeaderValue; -import static com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools.isSuccessWithContent; - -/** - * Use an HTTP endpoint as a stream, where the connection resetting is handled gracefully by reopening the connection - * and using a closed stream will just reopen the connection. - */ -public class PersistentHttpStream extends SeekableInputStream implements AutoCloseable { - - private static final Logger log = LoggerFactory.getLogger(com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream.class); - - private static final long MAX_SKIP_DISTANCE = 512L * 1024L; - protected final URI contentUrl; - private final HttpInterface httpInterface; - protected long position; - private int lastStatusCode; - private CloseableHttpResponse currentResponse; - private InputStream currentContent; - - /** - * @param httpInterface The HTTP interface to use for requests - * @param contentUrl The URL of the resource - * @param contentLength The length of the resource in bytes - */ - public PersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength) { - super(contentLength == null ? Units.CONTENT_LENGTH_UNKNOWN : contentLength, MAX_SKIP_DISTANCE); - - this.httpInterface = httpInterface; - this.contentUrl = contentUrl; - this.position = 0; - } - - private static boolean validateStatusCode(HttpResponse response, boolean returnOnServerError) { - int statusCode = response.getStatusLine().getStatusCode(); - if (returnOnServerError && statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR) { - return false; - } else if (!isSuccessWithContent(statusCode)) { - throw new RuntimeException("Not success status code: " + statusCode); - } - return true; - } - - /** - * Connect and return status code or return last status code if already connected. This causes the internal status - * code checker to be disabled, so non-success status codes will be returned instead of being thrown as they would - * be otherwise. - * - * @return The status code when connecting to the URL - * @throws IOException On IO error - */ - public int checkStatusCode() throws IOException { - connect(true); - - return lastStatusCode; - } - - /** - * @return An HTTP response if one is currently open. - */ - public HttpResponse getCurrentResponse() { - return currentResponse; - } - - protected URI getConnectUrl() { - return contentUrl; - } - - protected boolean useHeadersForRange() { - return true; - } - - private HttpGet getConnectRequest() { - HttpGet request = new HttpGet(getConnectUrl()); - - if (position > 0 && useHeadersForRange()) { - request.setHeader(HttpHeaders.RANGE, "bytes=" + position + "-"); - } - - return request; - } - - private void connect(boolean skipStatusCheck) throws IOException { - if (currentResponse == null) { - for (int i = 1; i >= 0; i--) { - if (attemptConnect(skipStatusCheck, i > 0)) { - break; - } - } - } - } - - public InputStream createContentInputStream(HttpResponse response) throws IOException { - return new BufferedInputStream(currentResponse.getEntity().getContent()); - } - - private boolean attemptConnect(boolean skipStatusCheck, boolean retryOnServerError) throws IOException { - currentResponse = httpInterface.execute(getConnectRequest()); - lastStatusCode = currentResponse.getStatusLine().getStatusCode(); - - if (!skipStatusCheck && !validateStatusCode(currentResponse, retryOnServerError)) { - return false; - } - - if (currentResponse.getEntity() == null) { - currentContent = EmptyInputStream.INSTANCE; - contentLength = 0; - return true; - } - - currentContent = createContentInputStream(currentResponse); - - if (contentLength == Units.CONTENT_LENGTH_UNKNOWN) { - Header header = currentResponse.getFirstHeader("Content-Length"); - - if (header != null) { - contentLength = Long.parseLong(header.getValue()); - } - } - - return true; - } - - private void handleNetworkException(IOException exception, boolean attemptReconnect) throws IOException { - if (!attemptReconnect || !HttpClientTools.isRetriableNetworkException(exception)) { - throw exception; - } - - close(); - - log.debug("Encountered retriable exception on url {}.", contentUrl, exception); - } - - private int internalRead(boolean attemptReconnect) throws IOException { - connect(false); - - try { - int result = currentContent.read(); - if (result >= 0) { - position++; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalRead(false); - } - } - - @Override - public int read() throws IOException { - return internalRead(true); - } - - private int internalRead(byte[] b, int off, int len, boolean attemptReconnect) throws IOException { - connect(false); - - try { - int result = currentContent.read(b, off, len); - if (result >= 0) { - position += result; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalRead(b, off, len, false); - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - return internalRead(b, off, len, true); - } - - private long internalSkip(long n, boolean attemptReconnect) throws IOException { - connect(false); - - try { - long result = currentContent.skip(n); - if (result >= 0) { - position += result; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalSkip(n, false); - } - } - - @Override - public long skip(long n) throws IOException { - return internalSkip(n, true); - } - - private int internalAvailable(boolean attemptReconnect) throws IOException { - connect(false); - - try { - return currentContent.available(); - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalAvailable(false); - } - } - - @Override - public int available() throws IOException { - return internalAvailable(true); - } - - @Override - public synchronized void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void close() throws IOException { - if (currentResponse != null) { - try { - currentResponse.close(); - } catch (IOException e) { - log.debug("Failed to close response.", e); - } - - currentResponse = null; - currentContent = null; - } - } - - /** - * Detach from the current connection, making sure not to close the connection when the stream is closed. - */ - public void releaseConnection() { - if (currentContent != null) { - try { - currentContent.close(); - } catch (IOException e) { - log.debug("Failed to close response stream.", e); - } - } - - currentResponse = null; - currentContent = null; - } - - @Override - public long getPosition() { - return position; - } - - @Override - protected void seekHard(long position) throws IOException { - close(); - - this.position = position; - } - - @Override - public boolean canSeekHard() { - return contentLength != Units.CONTENT_LENGTH_UNKNOWN; - } - - @Override - public List getTrackInfoProviders() { - if (currentResponse != null) { - return Collections.singletonList(createIceCastHeaderProvider()); - } else { - return Collections.emptyList(); - } - } - - private AudioTrackInfoProvider createIceCastHeaderProvider() { - AudioTrackInfoBuilder builder = AudioTrackInfoBuilder.empty() - .setTitle(getHeaderValue(currentResponse, "icy-description")) - .setAuthor(getHeaderValue(currentResponse, "icy-name")); - - if (builder.getTitle() == null) { - builder.setTitle(getHeaderValue(currentResponse, "icy-url")); - } - - return builder; - } - -} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java index 5c5ca6ff..ab90b4d2 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java @@ -10,39 +10,39 @@ public abstract class MirroringAudioSourceManager implements AudioSourceManager { - public static final String ISRC_PATTERN = "%ISRC%"; - public static final String QUERY_PATTERN = "%QUERY%"; - protected final AudioPlayerManager audioPlayerManager; - protected String[] providers = { - "ytsearch:\"" + ISRC_PATTERN + "\"", - "ytsearch:" + QUERY_PATTERN - }; - - protected MirroringAudioSourceManager(String[] providers, AudioPlayerManager audioPlayerManager) { - if (providers != null && providers.length > 0) { - this.providers = providers; - } - this.audioPlayerManager = audioPlayerManager; - } - - public String[] getProviders() { - return this.providers; - } - - public AudioPlayerManager getAudioPlayerManager() { - return this.audioPlayerManager; - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var isrcAudioTrack = ((MirroringAudioTrack) track); - DataFormatTools.writeNullableText(output, isrcAudioTrack.getISRC()); - DataFormatTools.writeNullableText(output, isrcAudioTrack.getArtworkURL()); - } + public static final String ISRC_PATTERN = "%ISRC%"; + public static final String QUERY_PATTERN = "%QUERY%"; + protected final AudioPlayerManager audioPlayerManager; + protected String[] providers = { + "ytsearch:\"" + ISRC_PATTERN + "\"", + "ytsearch:" + QUERY_PATTERN + }; + + protected MirroringAudioSourceManager(String[] providers, AudioPlayerManager audioPlayerManager) { + if (providers != null && providers.length > 0) { + this.providers = providers; + } + this.audioPlayerManager = audioPlayerManager; + } + + public String[] getProviders() { + return this.providers; + } + + public AudioPlayerManager getAudioPlayerManager() { + return this.audioPlayerManager; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var isrcAudioTrack = ((MirroringAudioTrack) track); + DataFormatTools.writeNullableText(output, isrcAudioTrack.getISRC()); + DataFormatTools.writeNullableText(output, isrcAudioTrack.getArtworkURL()); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java index 1731bfdf..0b76c1cb 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java @@ -23,110 +23,110 @@ public abstract class MirroringAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); - - protected final String isrc; - protected final String artworkURL; - protected final MirroringAudioSourceManager sourceManager; - - public MirroringAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, MirroringAudioSourceManager sourceManager) { - super(trackInfo); - this.isrc = isrc; - this.artworkURL = artworkURL; - this.sourceManager = sourceManager; - } - - public String getISRC() { - return this.isrc; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - private String getTrackTitle() { - var query = this.trackInfo.title; - if (!this.trackInfo.author.equals("unknown")) { - query += " " + this.trackInfo.author; - } - return query; - } - - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - AudioItem track = null; - - for (var provider : this.sourceManager.getProviders()) { - if (provider.startsWith(SpotifySourceManager.SEARCH_PREFIX)) { - log.warn("Can not use spotify search as search provider!"); - continue; - } - - if (provider.startsWith(AppleMusicSourceManager.SEARCH_PREFIX)) { - log.warn("Can not use apple music search as search provider!"); - continue; - } - - if (provider.contains(ISRC_PATTERN)) { - if (this.isrc != null) { - provider = provider.replace(ISRC_PATTERN, this.isrc); - } else { - log.debug("Ignoring identifier \"" + provider + "\" because this track does not have an ISRC!"); - continue; - } - } - - provider = provider.replace(QUERY_PATTERN, getTrackTitle()); - track = loadItem(provider); - if (track != AudioReference.NO_TRACK) { - break; - } - } - - if (track instanceof AudioPlaylist) { - track = ((AudioPlaylist) track).getTracks().get(0); - } - if (track instanceof InternalAudioTrack) { - processDelegate((InternalAudioTrack) track, executor); - return; - } - throw new FriendlyException("No matching track found", FriendlyException.Severity.COMMON, new TrackNotFoundException()); - } - - @Override - public AudioSourceManager getSourceManager() { - return this.sourceManager; - } - - private AudioItem loadItem(String query) { - var cf = new CompletableFuture(); - this.sourceManager.getAudioPlayerManager().loadItem(query, new AudioLoadResultHandler() { - - @Override - public void trackLoaded(AudioTrack track) { - log.debug("Track loaded: " + track.getIdentifier()); - cf.complete(track); - } - - @Override - public void playlistLoaded(AudioPlaylist playlist) { - log.debug("Playlist loaded: " + playlist.getName()); - cf.complete(playlist); - } - - @Override - public void noMatches() { - log.debug("No matches found for: " + query); - cf.complete(AudioReference.NO_TRACK); - } - - @Override - public void loadFailed(FriendlyException exception) { - log.debug("Failed to load: " + query); - cf.completeExceptionally(exception); - } - }); - return cf.join(); - } + private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); + + protected final String isrc; + protected final String artworkURL; + protected final MirroringAudioSourceManager sourceManager; + + public MirroringAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, MirroringAudioSourceManager sourceManager) { + super(trackInfo); + this.isrc = isrc; + this.artworkURL = artworkURL; + this.sourceManager = sourceManager; + } + + public String getISRC() { + return this.isrc; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + private String getTrackTitle() { + var query = this.trackInfo.title; + if (!this.trackInfo.author.equals("unknown")) { + query += " " + this.trackInfo.author; + } + return query; + } + + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + AudioItem track = null; + + for (var provider : this.sourceManager.getProviders()) { + if (provider.startsWith(SpotifySourceManager.SEARCH_PREFIX)) { + log.warn("Can not use spotify search as search provider!"); + continue; + } + + if (provider.startsWith(AppleMusicSourceManager.SEARCH_PREFIX)) { + log.warn("Can not use apple music search as search provider!"); + continue; + } + + if (provider.contains(ISRC_PATTERN)) { + if (this.isrc != null) { + provider = provider.replace(ISRC_PATTERN, this.isrc); + } else { + log.debug("Ignoring identifier \"" + provider + "\" because this track does not have an ISRC!"); + continue; + } + } + + provider = provider.replace(QUERY_PATTERN, getTrackTitle()); + track = loadItem(provider); + if (track != AudioReference.NO_TRACK) { + break; + } + } + + if (track instanceof AudioPlaylist) { + track = ((AudioPlaylist) track).getTracks().get(0); + } + if (track instanceof InternalAudioTrack) { + processDelegate((InternalAudioTrack) track, executor); + return; + } + throw new FriendlyException("No matching track found", FriendlyException.Severity.COMMON, new TrackNotFoundException()); + } + + @Override + public AudioSourceManager getSourceManager() { + return this.sourceManager; + } + + private AudioItem loadItem(String query) { + var cf = new CompletableFuture(); + this.sourceManager.getAudioPlayerManager().loadItem(query, new AudioLoadResultHandler() { + + @Override + public void trackLoaded(AudioTrack track) { + log.debug("Track loaded: " + track.getIdentifier()); + cf.complete(track); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) { + log.debug("Playlist loaded: " + playlist.getName()); + cf.complete(playlist); + } + + @Override + public void noMatches() { + log.debug("No matches found for: " + query); + cf.complete(AudioReference.NO_TRACK); + } + + @Override + public void loadFailed(FriendlyException exception) { + log.debug("Failed to load: " + query); + cf.completeExceptionally(exception); + } + }); + return cf.join(); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java index a2943fb6..52dcffa9 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/TrackNotFoundException.java @@ -2,6 +2,6 @@ public class TrackNotFoundException extends RuntimeException { - private static final long serialVersionUID = 6550093849278285754L; + private static final long serialVersionUID = 6550093849278285754L; } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java new file mode 100644 index 00000000..39bc5091 --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.spotify; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class SpotifyAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public SpotifyAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java index 1f5ac271..db02863d 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java @@ -6,13 +6,13 @@ public class SpotifyAudioTrack extends MirroringAudioTrack { - public SpotifyAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, SpotifySourceManager sourceManager) { - super(trackInfo, isrc, artworkURL, sourceManager); - } + public SpotifyAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, SpotifySourceManager sourceManager) { + super(trackInfo, isrc, artworkURL, sourceManager); + } - @Override - protected AudioTrack makeShallowClone() { - return new SpotifyAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (SpotifySourceManager) this.sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new SpotifyAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (SpotifySourceManager) this.sourceManager); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java index 19dd8c65..6ab41cf9 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java @@ -35,256 +35,263 @@ public class SpotifySourceManager extends MirroringAudioSourceManager implements HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?open\\.spotify\\.com/(user/[a-zA-Z0-9-_]+/)?(?track|album|playlist|artist)/(?[a-zA-Z0-9-_]+)"); - public static final String SEARCH_PREFIX = "spsearch:"; - public static final String RECOMMENDATIONS_PREFIX = "sprec:"; - public static final int PLAYLIST_MAX_PAGE_ITEMS = 100; - public static final int ALBUM_MAX_PAGE_ITEMS = 50; - public static final String API_BASE = "https://api.spotify.com/v1/"; - private static final Logger log = LoggerFactory.getLogger(SpotifySourceManager.class); - - private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - private final String clientId; - private final String clientSecret; - private final String countryCode; - private String token; - private Instant tokenExpire; - - public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager) { - super(providers, audioPlayerManager); - - if (clientId == null || clientId.isEmpty()) { - throw new IllegalArgumentException("Spotify client id must be set"); - } - this.clientId = clientId; - - if (clientSecret == null || clientSecret.isEmpty()) { - throw new IllegalArgumentException("Spotify secret must be set"); - } - this.clientSecret = clientSecret; - - if (countryCode == null || countryCode.isEmpty()) { - countryCode = "US"; - } - this.countryCode = countryCode; - } - - @Override - public String getSourceName() { - return "spotify"; - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new SpotifyAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); - } - - if (reference.identifier.startsWith(RECOMMENDATIONS_PREFIX)) { - return this.getRecommendations(reference.identifier.substring(RECOMMENDATIONS_PREFIX.length()).trim()); - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (!matcher.find()) { - return null; - } - - var id = matcher.group("identifier"); - switch (matcher.group("type")) { - case "album": - return this.getAlbum(id); - - case "track": - return this.getTrack(id); - - case "playlist": - return this.getPlaylist(id); - - case "artist": - return this.getArtist(id); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - public void requestToken() throws IOException { - var request = new HttpPost("https://accounts.spotify.com/api/token"); - request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8))); - request.setEntity(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("grant_type", "client_credentials")), StandardCharsets.UTF_8)); - - var json = HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - this.token = json.get("access_token").text(); - this.tokenExpire = Instant.now().plusSeconds(json.get("expires_in").asLong(0)); - } - - public String getToken() throws IOException { - if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { - this.requestToken(); - } - return this.token; - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.addHeader("Authorization", "Bearer " + this.getToken()); - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - public AudioItem getSearch(String query) throws IOException { - var json = this.getJson(API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track"); - if (json == null || json.get("tracks").get("items").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist("Search results for: " + query, this.parseTrackItems(json.get("tracks")), null, true); - } - - public AudioItem getRecommendations(String query) throws IOException { - var json = this.getJson(API_BASE + "recommendations?" + query); - if (json == null || json.get("tracks").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist("Spotify Recommendations:", this.parseTracks(json), null, false); - } - - public AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(API_BASE + "albums/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "albums/" + id + "/tracks?limit=" + ALBUM_MAX_PAGE_ITEMS + "&offset=" + offset); - offset += ALBUM_MAX_PAGE_ITEMS; - - tracks.addAll(this.parseTrackItems(page)); - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("name").text(), tracks, null, false); - - } - - public AudioItem getPlaylist(String id) throws IOException { - var json = this.getJson(API_BASE + "playlists/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - - var tracks = new ArrayList(); - JsonBrowser page; - var offset = 0; - do { - page = this.getJson(API_BASE + "playlists/" + id + "/tracks?limit=" + PLAYLIST_MAX_PAGE_ITEMS + "&offset=" + offset); - offset += PLAYLIST_MAX_PAGE_ITEMS; - - for (var value : page.get("items").values()) { - var track = value.get("track"); - if (track.isNull() || track.get("is_local").asBoolean(false)) { - continue; - } - tracks.add(this.parseTrack(track)); - } - - } - while (page.get("next").text() != null); - - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - - return new BasicAudioPlaylist(json.get("name").text(), tracks, null, false); - - } - - public AudioItem getArtist(String id) throws IOException { - var json = this.getJson(API_BASE + "artists/" + id + "/top-tracks?market=" + this.countryCode); - if (json == null || json.get("tracks").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("tracks").index(0).get("artists").index(0).get("name").text() + "'s Top Tracks", this.parseTracks(json), null, false); - } - - public AudioItem getTrack(String id) throws IOException { - var json = this.getJson(API_BASE + "tracks/" + id); - if (json == null) { - return AudioReference.NO_TRACK; - } - return parseTrack(json); - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var value : json.get("tracks").values()) { - tracks.add(this.parseTrack(value)); - } - return tracks; - } - - private List parseTrackItems(JsonBrowser json) { - var tracks = new ArrayList(); - for (var value : json.get("items").values()) { - if (value.get("is_local").asBoolean(false)) { - continue; - } - tracks.add(this.parseTrack(value)); - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - return new SpotifyAudioTrack( - new AudioTrackInfo( - json.get("name").text(), - json.get("artists").index(0).get("name").text(), - json.get("duration_ms").asLong(0), - json.get("id").text(), - false, - json.get("external_urls").get("spotify").text() - ), - json.get("external_ids").get("isrc").text(), - json.get("album").get("images").index(0).get("url").text(), - this - ); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?open\\.spotify\\.com/(user/[a-zA-Z0-9-_]+/)?(?track|album|playlist|artist)/(?[a-zA-Z0-9-_]+)"); + public static final String SEARCH_PREFIX = "spsearch:"; + public static final String RECOMMENDATIONS_PREFIX = "sprec:"; + public static final int PLAYLIST_MAX_PAGE_ITEMS = 100; + public static final int ALBUM_MAX_PAGE_ITEMS = 50; + public static final String API_BASE = "https://api.spotify.com/v1/"; + private static final Logger log = LoggerFactory.getLogger(SpotifySourceManager.class); + + private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + private final String clientId; + private final String clientSecret; + private final String countryCode; + private String token; + private Instant tokenExpire; + + public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager) { + super(providers, audioPlayerManager); + + if (clientId == null || clientId.isEmpty()) { + throw new IllegalArgumentException("Spotify client id must be set"); + } + this.clientId = clientId; + + if (clientSecret == null || clientSecret.isEmpty()) { + throw new IllegalArgumentException("Spotify secret must be set"); + } + this.clientSecret = clientSecret; + + if (countryCode == null || countryCode.isEmpty()) { + countryCode = "US"; + } + this.countryCode = countryCode; + } + + @Override + public String getSourceName() { + return "spotify"; + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new SpotifyAudioTrack(trackInfo, + DataFormatTools.readNullableText(input), + DataFormatTools.readNullableText(input), + this + ); + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); + } + + if (reference.identifier.startsWith(RECOMMENDATIONS_PREFIX)) { + return this.getRecommendations(reference.identifier.substring(RECOMMENDATIONS_PREFIX.length()).trim()); + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (!matcher.find()) { + return null; + } + + var id = matcher.group("identifier"); + switch (matcher.group("type")) { + case "album": + return this.getAlbum(id); + + case "track": + return this.getTrack(id); + + case "playlist": + return this.getPlaylist(id); + + case "artist": + return this.getArtist(id); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + public void requestToken() throws IOException { + var request = new HttpPost("https://accounts.spotify.com/api/token"); + request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8))); + request.setEntity(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("grant_type", "client_credentials")), StandardCharsets.UTF_8)); + + var json = HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + this.token = json.get("access_token").text(); + this.tokenExpire = Instant.now().plusSeconds(json.get("expires_in").asLong(0)); + } + + public String getToken() throws IOException { + if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { + this.requestToken(); + } + return this.token; + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.addHeader("Authorization", "Bearer " + this.getToken()); + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + public AudioItem getSearch(String query) throws IOException { + var json = this.getJson(API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track"); + if (json == null || json.get("tracks").get("items").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + return new BasicAudioPlaylist("Search results for: " + query, this.parseTrackItems(json.get("tracks")), null, true); + } + + public AudioItem getRecommendations(String query) throws IOException { + var json = this.getJson(API_BASE + "recommendations?" + query); + if (json == null || json.get("tracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + return new BasicAudioPlaylist("Spotify Recommendations:", this.parseTracks(json), null, false); + } + + public AudioItem getAlbum(String id) throws IOException { + var json = this.getJson(API_BASE + "albums/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "albums/" + id + "/tracks?limit=" + ALBUM_MAX_PAGE_ITEMS + "&offset=" + offset); + offset += ALBUM_MAX_PAGE_ITEMS; + + tracks.addAll(this.parseTrackItems(page)); + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("images").index(0).get("url").text(); + var author = json.get("author").get("name").text(); + return new SpotifyAudioPlaylist(json.get("name").text(), tracks, "album", id, artworkUrl, author); + + } + + public AudioItem getPlaylist(String id) throws IOException { + var json = this.getJson(API_BASE + "playlists/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + + var tracks = new ArrayList(); + JsonBrowser page; + var offset = 0; + do { + page = this.getJson(API_BASE + "playlists/" + id + "/tracks?limit=" + PLAYLIST_MAX_PAGE_ITEMS + "&offset=" + offset); + offset += PLAYLIST_MAX_PAGE_ITEMS; + + for (var value : page.get("items").values()) { + var track = value.get("track"); + if (track.isNull() || track.get("is_local").asBoolean(false)) { + continue; + } + tracks.add(this.parseTrack(track)); + } + + } + while (page.get("next").text() != null); + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("images").index(0).get("url").text(); + var author = json.get("owner").get("string").text(); + return new SpotifyAudioPlaylist(json.get("name").text(), tracks, "playlist", id, artworkUrl, author); + + } + + public AudioItem getArtist(String id) throws IOException { + var json = this.getJson(API_BASE + "artists/" + id + "/top-tracks?market=" + this.countryCode); + if (json == null || json.get("tracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + + var artworkUrl = json.get("tracks").index(0).get("album").get("images").index(0).get("url").text(); + var author = json.get("tracks").index(0).get("artists").index(0).get("name").text(); + return new SpotifyAudioPlaylist(author + "'s Top Tracks", this.parseTracks(json), "artist", id, artworkUrl, author); + } + + public AudioItem getTrack(String id) throws IOException { + var json = this.getJson(API_BASE + "tracks/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } + return parseTrack(json); + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var value : json.get("tracks").values()) { + tracks.add(this.parseTrack(value)); + } + return tracks; + } + + private List parseTrackItems(JsonBrowser json) { + var tracks = new ArrayList(); + for (var value : json.get("items").values()) { + if (value.get("is_local").asBoolean(false)) { + continue; + } + tracks.add(this.parseTrack(value)); + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + return new SpotifyAudioTrack( + new AudioTrackInfo( + json.get("name").text(), + json.get("artists").index(0).get("name").text(), + json.get("duration_ms").asLong(0), + json.get("id").text(), + false, + json.get("external_urls").get("spotify").text() + ), + json.get("external_ids").get("isrc").text(), + json.get("album").get("images").index(0).get("url").text(), + this + ); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java new file mode 100644 index 00000000..c461018b --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java @@ -0,0 +1,39 @@ +package com.github.topisenpai.lavasrc.yandexmusic; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class YandexMusicAudioPlaylist extends BasicAudioPlaylist { + + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public YandexMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java index 433ce6e6..a7ed0f6b 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioTrack.java @@ -17,66 +17,66 @@ import java.security.NoSuchAlgorithmException; public class YandexMusicAudioTrack extends DelegatedAudioTrack { - private final String artworkURL; - private final YandexMusicSourceManager sourceManager; + private final String artworkURL; + private final YandexMusicSourceManager sourceManager; - public YandexMusicAudioTrack(AudioTrackInfo trackInfo, String artworkURL, YandexMusicSourceManager sourceManager) { - super(trackInfo); - this.artworkURL = artworkURL; - this.sourceManager = sourceManager; - } + public YandexMusicAudioTrack(AudioTrackInfo trackInfo, String artworkURL, YandexMusicSourceManager sourceManager) { + super(trackInfo); + this.artworkURL = artworkURL; + this.sourceManager = sourceManager; + } - public String getArtworkURL() { - return this.artworkURL; - } + public String getArtworkURL() { + return this.artworkURL; + } - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - var downloadLink = this.getDownloadURL(this.trackInfo.identifier); - try (var httpInterface = this.sourceManager.getHttpInterface()) { - try (var stream = new PersistentHttpStream(httpInterface, new URI(downloadLink), this.trackInfo.length)) { - processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); - } - } - } + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + var downloadLink = this.getDownloadURL(this.trackInfo.identifier); + try (var httpInterface = this.sourceManager.getHttpInterface()) { + try (var stream = new PersistentHttpStream(httpInterface, new URI(downloadLink), this.trackInfo.length)) { + processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); + } + } + } - @Override - protected AudioTrack makeShallowClone() { - return new YandexMusicAudioTrack(this.trackInfo, this.artworkURL, this.sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new YandexMusicAudioTrack(this.trackInfo, this.artworkURL, this.sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return this.sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return this.sourceManager; + } - private String getDownloadURL(String id) throws IOException, NoSuchAlgorithmException { - var json = this.sourceManager.getJson(YandexMusicSourceManager.PUBLIC_API_BASE + "/tracks/" + id + "/download-info"); - if (json.isNull() || json.get("result").values().isEmpty()) { - throw new IllegalStateException("No download URL found for track " + id); - } + private String getDownloadURL(String id) throws IOException, NoSuchAlgorithmException { + var json = this.sourceManager.getJson(YandexMusicSourceManager.PUBLIC_API_BASE + "/tracks/" + id + "/download-info"); + if (json.isNull() || json.get("result").values().isEmpty()) { + throw new IllegalStateException("No download URL found for track " + id); + } - var downloadInfoLink = json.get("result").values().get(0).get("downloadInfoUrl").text(); - var downloadInfo = this.sourceManager.getDownloadStrings(downloadInfoLink); - if (downloadInfo == null) { - throw new IllegalStateException("No download URL found for track " + id); - } + var downloadInfoLink = json.get("result").values().get(0).get("downloadInfoUrl").text(); + var downloadInfo = this.sourceManager.getDownloadStrings(downloadInfoLink); + if (downloadInfo == null) { + throw new IllegalStateException("No download URL found for track " + id); + } - var doc = Jsoup.parse(downloadInfo, "", Parser.xmlParser()); - var host = doc.select("host").text(); - var path = doc.select("path").text(); - var ts = doc.select("ts").text(); - var s = doc.select("s").text(); + var doc = Jsoup.parse(downloadInfo, "", Parser.xmlParser()); + var host = doc.select("host").text(); + var path = doc.select("path").text(); + var ts = doc.select("ts").text(); + var s = doc.select("s").text(); - var sign = "XGRlBW9FXlekgbPrRHuSiA" + path + s; - var md = MessageDigest.getInstance("MD5"); - var digest = md.digest(sign.getBytes(StandardCharsets.UTF_8)); - var sb = new StringBuilder(); - for (byte b : digest) { - sb.append(String.format("%02x", b)); - } - var md5 = sb.toString(); + var sign = "XGRlBW9FXlekgbPrRHuSiA" + path + s; + var md = MessageDigest.getInstance("MD5"); + var digest = md.digest(sign.getBytes(StandardCharsets.UTF_8)); + var sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + var md5 = sb.toString(); - return "https://" + host + "/get-mp3/" + md5 + "/" + ts + path; - } + return "https://" + host + "/get-mp3/" + md5 + "/" + ts + path; + } } \ No newline at end of file diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java index 4feae52f..71c63ab6 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicSourceManager.java @@ -31,211 +31,221 @@ import java.util.regex.Pattern; public class YandexMusicSourceManager implements AudioSourceManager, HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/(?artist|album)/(?[0-9]+)/?((?track/)(?[0-9]+)/?)?"); - public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/users/(?[0-9A-Za-z@.-]+)/playlists/(?[0-9]+)/?"); - public static final String SEARCH_PREFIX = "ymsearch:"; - public static final String PUBLIC_API_BASE = "https://api.music.yandex.net"; - - private static final Logger log = LoggerFactory.getLogger(YandexMusicSourceManager.class); - - private final HttpInterfaceManager httpInterfaceManager; - - private final String accessToken; - - public YandexMusicSourceManager(String accessToken) { - if (accessToken == null || accessToken.isEmpty()) { - throw new IllegalArgumentException("Yandex Music accessToken must be set"); - } - this.accessToken = accessToken; - this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } - - @Override - public String getSourceName() { - return "yandexmusic"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); - } - - var matcher = URL_PATTERN.matcher(reference.identifier); - if (matcher.find()) { - switch (matcher.group("type1")) { - case "album": - if (matcher.group("type2") != null) { - var trackId = matcher.group("identifier2"); - return this.getTrack(trackId); - } - var albumId = matcher.group("identifier"); - return this.getAlbum(albumId); - case "artist": - var artistId = matcher.group("identifier"); - return this.getArtist(artistId); - } - return null; - } - matcher = URL_PLAYLIST_PATTERN.matcher(reference.identifier); - if (matcher.find()) { - var userId = matcher.group("identifier"); - var playlistId = matcher.group("identifier2"); - return this.getPlaylist(userId, playlistId); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - - private AudioItem getSearch(String query) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/search?text=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track&page=0"); - if (json.isNull() || json.get("result").get("tracks").isNull()) { - return AudioReference.NO_TRACK; - } - var tracks = this.parseTracks(json.get("result").get("tracks").get("results")); - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist("Yandex Music Search: " + query, tracks, null, true); - } - - private AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/albums/" + id + "/with-tracks"); - if (json.isNull() || json.get("result").isNull()) { - return AudioReference.NO_TRACK; - } - var tracks = new ArrayList(); - for (var volume : json.get("result").get("volumes").values()) { - for (var track : volume.values()) { - tracks.add(this.parseTrack(track)); - } - } - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(json.get("result").get("title").text(), tracks, null, false); - } - - private AudioItem getTrack(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/tracks/" + id); - if (json.isNull() || json.get("result").values().get(0).get("available").text().equals("false")) { - return AudioReference.NO_TRACK; - } - return this.parseTrack(json.get("result").values().get(0)); - } - - private AudioItem getArtist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/artists/" + id + "/tracks?page-size=10"); - if (json.isNull() || json.get("result").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - var artistName = this.getJson(PUBLIC_API_BASE + "/artists/" + id).get("result").get("artist").get("name").text(); - var tracks = this.parseTracks(json.get("result").get("tracks")); - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - return new BasicAudioPlaylist(artistName + "'s Top Tracks", tracks, null, false); - } - - private AudioItem getPlaylist(String userString, String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/users/" + userString + "/playlists/" + id); - if (json.isNull() || json.get("result").isNull() || json.get("result").get("tracks").values().isEmpty()) { - return AudioReference.NO_TRACK; - } - var tracks = new ArrayList(); - for (var track : json.get("result").get("tracks").values()) { - tracks.add(this.parseTrack(track.get("track"))); - } - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } - var playlist_title = json.get("result").get("kind").text().equals("3") ? "Liked songs" : json.get("result").get("title").text(); - return new BasicAudioPlaylist(playlist_title, tracks, null, false); - } - - public JsonBrowser getJson(String uri) throws IOException { - var request = new HttpGet(uri); - request.setHeader("Accept", "application/json"); - request.setHeader("Authorization", "OAuth " + this.accessToken); - return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); - } - - public String getDownloadStrings(String uri) throws IOException { - var request = new HttpGet(uri); - request.setHeader("Accept", "application/json"); - request.setHeader("Authorization", "OAuth " + this.accessToken); - return HttpClientTools.fetchResponseLines(this.httpInterfaceManager.getInterface(), request, "downloadinfo-xml-page")[0]; - } - - private List parseTracks(JsonBrowser json) { - var tracks = new ArrayList(); - for (var track : json.values()) { - var parsedTrack = this.parseTrack(track); - if (parsedTrack != null) { - tracks.add(parsedTrack); - } - } - return tracks; - } - - private AudioTrack parseTrack(JsonBrowser json) { - if (!json.get("available").asBoolean(false) || json.get("albums").values().isEmpty()) { - return null; - } - var id = json.get("id").text(); - var artist = json.get("major").get("name").text().equals("PODCASTS") ? json.get("albums").values().get(0).get("title").text() : json.get("artists").values().get(0).get("name").text(); - var coverUri = json.get("albums").values().get(0).get("coverUri").text(); - return new YandexMusicAudioTrack(new AudioTrackInfo( - json.get("title").text(), - artist, - json.get("durationMs").as(Long.class), - id, - false, - "https://music.yandex.ru/album/" + json.get("albums").values().get(0).get("id").text() + "/track/" + id), - coverUri != null ? "https://" + coverUri.replace("%%", "400x400") : null, - this - ); - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var yandexMusicAudioTrack = ((YandexMusicAudioTrack) track); - DataFormatTools.writeNullableText(output, yandexMusicAudioTrack.getArtworkURL()); - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new YandexMusicAudioTrack(trackInfo, DataFormatTools.readNullableText(input), this); - } - - @Override - public void configureRequests(Function configurator) { - this.httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - this.httpInterfaceManager.configureBuilder(configurator); - } - - @Override - public void shutdown() { - try { - this.httpInterfaceManager.close(); - } catch (IOException e) { - log.error("Failed to close HTTP interface manager", e); - } - } - - public HttpInterface getHttpInterface() { - return this.httpInterfaceManager.getInterface(); - } + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/(?artist|album)/(?[0-9]+)/?((?track/)(?[0-9]+)/?)?"); + public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.ru/users/(?[0-9A-Za-z@.-]+)/playlists/(?[0-9]+)/?"); + public static final String SEARCH_PREFIX = "ymsearch:"; + public static final String PUBLIC_API_BASE = "https://api.music.yandex.net"; + + private static final Logger log = LoggerFactory.getLogger(YandexMusicSourceManager.class); + + private final HttpInterfaceManager httpInterfaceManager; + + private final String accessToken; + + public YandexMusicSourceManager(String accessToken) { + if (accessToken == null || accessToken.isEmpty()) { + throw new IllegalArgumentException("Yandex Music accessToken must be set"); + } + this.accessToken = accessToken; + this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "yandexmusic"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); + } + + var matcher = URL_PATTERN.matcher(reference.identifier); + if (matcher.find()) { + switch (matcher.group("type1")) { + case "album": + if (matcher.group("type2") != null) { + var trackId = matcher.group("identifier2"); + return this.getTrack(trackId); + } + var albumId = matcher.group("identifier"); + return this.getAlbum(albumId); + case "artist": + var artistId = matcher.group("identifier"); + return this.getArtist(artistId); + } + return null; + } + matcher = URL_PLAYLIST_PATTERN.matcher(reference.identifier); + if (matcher.find()) { + var userId = matcher.group("identifier"); + var playlistId = matcher.group("identifier2"); + return this.getPlaylist(userId, playlistId); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + private AudioItem getSearch(String query) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/search?text=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track&page=0"); + if (json.isNull() || json.get("result").get("tracks").isNull()) { + return AudioReference.NO_TRACK; + } + var tracks = this.parseTracks(json.get("result").get("tracks").get("results")); + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + return new BasicAudioPlaylist("Yandex Music Search: " + query, tracks, null, true); + } + + private AudioItem getAlbum(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/albums/" + id + "/with-tracks"); + if (json.isNull() || json.get("result").isNull()) { + return AudioReference.NO_TRACK; + } + var tracks = new ArrayList(); + for (var volume : json.get("result").get("volumes").values()) { + for (var track : volume.values()) { + tracks.add(this.parseTrack(track)); + } + } + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var coverUri = json.get("result").get("coverUri").text(); + var author = json.get("result").get("artists").values().get(0).get("name").text(); + return new YandexMusicAudioPlaylist(json.get("result").get("title").text(), tracks, "album", id, this.formatCoverUri(coverUri), author); + } + + private AudioItem getTrack(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/tracks/" + id); + if (json.isNull() || json.get("result").values().get(0).get("available").text().equals("false")) { + return AudioReference.NO_TRACK; + } + return this.parseTrack(json.get("result").values().get(0)); + } + + private AudioItem getArtist(String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/artists/" + id + "/tracks?page-size=10"); + if (json.isNull() || json.get("result").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + var tracks = this.parseTracks(json.get("result").get("tracks")); + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var artistJson = this.getJson(PUBLIC_API_BASE + "/artists/" + id); + var coverUri = json.get("result").get("coverUri").text(); + var author = artistJson.get("result").get("artist").get("name").text(); + return new YandexMusicAudioPlaylist(author + "'s Top Tracks", tracks, "artist", id, this.formatCoverUri(coverUri), author); + } + + private AudioItem getPlaylist(String userString, String id) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/users/" + userString + "/playlists/" + id); + if (json.isNull() || json.get("result").isNull() || json.get("result").get("tracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + var tracks = new ArrayList(); + for (var track : json.get("result").get("tracks").values()) { + tracks.add(this.parseTrack(track.get("track"))); + } + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var playlistTitle = json.get("result").get("kind").text().equals("3") ? "Liked songs" : json.get("result").get("title").text(); + var coverUri = json.get("result").get("cover").get("uri").text(); + var author = json.get("result").get("owner").get("name").text(); + return new YandexMusicAudioPlaylist(playlistTitle, tracks, "playlist", id, this.formatCoverUri(coverUri), author); + } + + public JsonBrowser getJson(String uri) throws IOException { + var request = new HttpGet(uri); + request.setHeader("Accept", "application/json"); + request.setHeader("Authorization", "OAuth " + this.accessToken); + return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); + } + + public String getDownloadStrings(String uri) throws IOException { + var request = new HttpGet(uri); + request.setHeader("Accept", "application/json"); + request.setHeader("Authorization", "OAuth " + this.accessToken); + return HttpClientTools.fetchResponseLines(this.httpInterfaceManager.getInterface(), request, "downloadinfo-xml-page")[0]; + } + + private List parseTracks(JsonBrowser json) { + var tracks = new ArrayList(); + for (var track : json.values()) { + var parsedTrack = this.parseTrack(track); + if (parsedTrack != null) { + tracks.add(parsedTrack); + } + } + return tracks; + } + + private AudioTrack parseTrack(JsonBrowser json) { + if (!json.get("available").asBoolean(false) || json.get("albums").values().isEmpty()) { + return null; + } + var id = json.get("id").text(); + var artist = json.get("major").get("name").text().equals("PODCASTS") ? json.get("albums").values().get(0).get("title").text() : json.get("artists").values().get(0).get("name").text(); + var coverUri = json.get("coverUri").text(); + return new YandexMusicAudioTrack(new AudioTrackInfo( + json.get("title").text(), + artist, + json.get("durationMs").as(Long.class), + id, + false, + "https://music.yandex.ru/album/" + json.get("albums").values().get(0).get("id").text() + "/track/" + id), + this.formatCoverUri(coverUri), + this + ); + } + + private String formatCoverUri(String coverUri) { + return coverUri != null ? "https://" + coverUri.replace("%%", "400x400") : null; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var yandexMusicAudioTrack = ((YandexMusicAudioTrack) track); + DataFormatTools.writeNullableText(output, yandexMusicAudioTrack.getArtworkURL()); + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new YandexMusicAudioTrack(trackInfo, DataFormatTools.readNullableText(input), this); + } + + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + + public HttpInterface getHttpInterface() { + return this.httpInterfaceManager.getInterface(); + } } \ No newline at end of file diff --git a/plugin/build.gradle b/plugin/build.gradle index a05ac57d..eece8156 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -9,7 +9,7 @@ var moduleName = "lavasrc-plugin" mainClassName = "org.springframework.boot.loader.JarLauncher" dependencies { - compileOnly("dev.arbjerg.lavalink:plugin-api:3.6.1") + compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:20b8030") runtimeOnly("com.github.freyacodes.lavalink:Lavalink-Server:3.6.0") implementation project(":main") } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java index bc1e730e..c5076ebf 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/AppleMusicConfig.java @@ -7,23 +7,23 @@ @Component public class AppleMusicConfig { - private String countryCode = "us"; - private String mediaAPIToken; + private String countryCode = "us"; + private String mediaAPIToken; - public String getCountryCode() { - return this.countryCode; - } + public String getCountryCode() { + return this.countryCode; + } - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } - public String getMediaAPIToken() { - return this.mediaAPIToken; - } + public String getMediaAPIToken() { + return this.mediaAPIToken; + } - public void setMediaAPIToken(String mediaAPIToken) { - this.mediaAPIToken = mediaAPIToken; - } + public void setMediaAPIToken(String mediaAPIToken) { + this.mediaAPIToken = mediaAPIToken; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java index 7258bbb5..d9f81f37 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/DeezerConfig.java @@ -7,14 +7,14 @@ @Component public class DeezerConfig { - private String masterDecryptionKey; + private String masterDecryptionKey; - public String getMasterDecryptionKey() { - return this.masterDecryptionKey; - } + public String getMasterDecryptionKey() { + return this.masterDecryptionKey; + } - public void setMasterDecryptionKey(String masterDecryptionKey) { - this.masterDecryptionKey = masterDecryptionKey; - } + public void setMasterDecryptionKey(String masterDecryptionKey) { + this.masterDecryptionKey = masterDecryptionKey; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java index c5fc3da2..17ee83d8 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcConfig.java @@ -10,17 +10,17 @@ @Component public class LavaSrcConfig { - private String[] providers = { - "ytsearch:\"" + ISRC_PATTERN + "\"", - "ytsearch:" + QUERY_PATTERN - }; + private String[] providers = { + "ytsearch:\"" + ISRC_PATTERN + "\"", + "ytsearch:" + QUERY_PATTERN + }; - public String[] getProviders() { - return this.providers; - } + public String[] getProviders() { + return this.providers; + } - public void setProviders(String[] providers) { - this.providers = providers; - } + public void setProviders(String[] providers) { + this.providers = providers; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java new file mode 100644 index 00000000..7d75a4bf --- /dev/null +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java @@ -0,0 +1,86 @@ +package com.github.topisenpai.lavasrc.plugin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioPlaylist; +import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioTrack; +import com.github.topisenpai.lavasrc.deezer.DeezerAudioPlaylist; +import com.github.topisenpai.lavasrc.deezer.DeezerAudioTrack; +import com.github.topisenpai.lavasrc.spotify.SpotifyAudioPlaylist; +import com.github.topisenpai.lavasrc.spotify.SpotifyAudioTrack; +import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioPlaylist; +import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import dev.arbjerg.lavalink.api.AudioPlaylistJsonAppender; +import dev.arbjerg.lavalink.api.AudioTrackJsonAppender; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class LavaSrcJsonAppender implements AudioTrackJsonAppender, AudioPlaylistJsonAppender { + + @Override + public Map appendFields(AudioPlaylist playlist) { + var map = new HashMap(); + + if (playlist instanceof SpotifyAudioPlaylist) { + var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; + map.put("type", new TextNode(spotifyPlaylist.getType())); + map.put("identifier", new TextNode(spotifyPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(spotifyPlaylist.getArtworkURL())); + map.put("author", new TextNode(spotifyPlaylist.getAuthor())); + + } else if (playlist instanceof AppleMusicAudioPlaylist) { + var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; + map.put("type", new TextNode(appleMusicPlaylist.getType())); + map.put("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(appleMusicPlaylist.getArtworkURL())); + map.put("author", new TextNode(appleMusicPlaylist.getAuthor())); + + } else if (playlist instanceof DeezerAudioPlaylist) { + var deezerPlaylist = (DeezerAudioPlaylist) playlist; + map.put("type", new TextNode(deezerPlaylist.getType())); + map.put("identifier", new TextNode(deezerPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(deezerPlaylist.getArtworkURL())); + map.put("author", new TextNode(deezerPlaylist.getAuthor())); + + } else if (playlist instanceof YandexMusicAudioPlaylist) { + var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; + map.put("type", new TextNode(yandexMusicPlaylist.getType())); + map.put("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); + map.put("artworkURL", new TextNode(yandexMusicPlaylist.getArtworkURL())); + map.put("author", new TextNode(yandexMusicPlaylist.getAuthor())); + } + + return map; + } + + @Override + public Map appendFields(AudioTrack audioTrack) { + var map = new HashMap(); + + if (audioTrack instanceof SpotifyAudioTrack) { + var spotifyAudioTrack = (SpotifyAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(spotifyAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(spotifyAudioTrack.getISRC())); + + } else if (audioTrack instanceof AppleMusicAudioTrack) { + var appleMusicAudioTrack = (AppleMusicAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(appleMusicAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(appleMusicAudioTrack.getISRC())); + + } else if (audioTrack instanceof DeezerAudioTrack) { + var deezerAudioTrack = (DeezerAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(deezerAudioTrack.getArtworkURL())); + map.put("isrc", new TextNode(deezerAudioTrack.getISRC())); + } else if (audioTrack instanceof YandexMusicAudioTrack) { + var yandexMusicAudioTrack = (YandexMusicAudioTrack) audioTrack; + map.put("artworkURL", new TextNode(yandexMusicAudioTrack.getArtworkURL())); + } + + return map; + } +} diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java index e2fbcd06..e26616cf 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPlugin.java @@ -13,45 +13,45 @@ @Service public class LavaSrcPlugin implements AudioPlayerManagerConfiguration { - private static final Logger log = LoggerFactory.getLogger(LavaSrcPlugin.class); - - private final LavaSrcConfig pluginConfig; - private final SourcesConfig sourcesConfig; - private final SpotifyConfig spotifyConfig; - private final AppleMusicConfig appleMusicConfig; - private final YandexMusicConfig yandexMusicConfig; - - private final DeezerConfig deezerConfig; - - public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, SpotifyConfig spotifyConfig, AppleMusicConfig appleMusicConfig, DeezerConfig deezerConfig, YandexMusicConfig yandexMusicConfig) { - log.info("Loading LavaSrc plugin..."); - this.pluginConfig = pluginConfig; - this.sourcesConfig = sourcesConfig; - this.spotifyConfig = spotifyConfig; - this.appleMusicConfig = appleMusicConfig; - this.deezerConfig = deezerConfig; - this.yandexMusicConfig = yandexMusicConfig; - } - - @Override - public AudioPlayerManager configure(AudioPlayerManager manager) { - if (this.sourcesConfig.isSpotify()) { - log.info("Registering Spotify audio source manager..."); - manager.registerSourceManager(new SpotifySourceManager(this.pluginConfig.getProviders(), this.spotifyConfig.getClientId(), this.spotifyConfig.getClientSecret(), this.spotifyConfig.getCountryCode(), manager)); - } - if (this.sourcesConfig.isAppleMusic()) { - log.info("Registering Apple Music audio source manager..."); - manager.registerSourceManager(new AppleMusicSourceManager(this.pluginConfig.getProviders(), this.appleMusicConfig.getMediaAPIToken(), this.appleMusicConfig.getCountryCode(), manager)); - } - if (this.sourcesConfig.isDeezer()) { - log.info("Registering Deezer audio source manager..."); - manager.registerSourceManager(new DeezerAudioSourceManager(this.deezerConfig.getMasterDecryptionKey())); - } - if (this.sourcesConfig.isYandexMusic()) { - log.info("Registering Yandex Music audio source manager..."); - manager.registerSourceManager(new YandexMusicSourceManager(this.yandexMusicConfig.getAccessToken())); - } - return manager; - } + private static final Logger log = LoggerFactory.getLogger(LavaSrcPlugin.class); + + private final LavaSrcConfig pluginConfig; + private final SourcesConfig sourcesConfig; + private final SpotifyConfig spotifyConfig; + private final AppleMusicConfig appleMusicConfig; + private final YandexMusicConfig yandexMusicConfig; + + private final DeezerConfig deezerConfig; + + public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, SpotifyConfig spotifyConfig, AppleMusicConfig appleMusicConfig, DeezerConfig deezerConfig, YandexMusicConfig yandexMusicConfig) { + log.info("Loading LavaSrc plugin..."); + this.pluginConfig = pluginConfig; + this.sourcesConfig = sourcesConfig; + this.spotifyConfig = spotifyConfig; + this.appleMusicConfig = appleMusicConfig; + this.deezerConfig = deezerConfig; + this.yandexMusicConfig = yandexMusicConfig; + } + + @Override + public AudioPlayerManager configure(AudioPlayerManager manager) { + if (this.sourcesConfig.isSpotify()) { + log.info("Registering Spotify audio source manager..."); + manager.registerSourceManager(new SpotifySourceManager(this.pluginConfig.getProviders(), this.spotifyConfig.getClientId(), this.spotifyConfig.getClientSecret(), this.spotifyConfig.getCountryCode(), manager)); + } + if (this.sourcesConfig.isAppleMusic()) { + log.info("Registering Apple Music audio source manager..."); + manager.registerSourceManager(new AppleMusicSourceManager(this.pluginConfig.getProviders(), this.appleMusicConfig.getMediaAPIToken(), this.appleMusicConfig.getCountryCode(), manager)); + } + if (this.sourcesConfig.isDeezer()) { + log.info("Registering Deezer audio source manager..."); + manager.registerSourceManager(new DeezerAudioSourceManager(this.deezerConfig.getMasterDecryptionKey())); + } + if (this.sourcesConfig.isYandexMusic()) { + log.info("Registering Yandex Music audio source manager..."); + manager.registerSourceManager(new YandexMusicSourceManager(this.yandexMusicConfig.getAccessToken())); + } + return manager; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java index 45ea6bd1..2fcd7c28 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SourcesConfig.java @@ -8,40 +8,40 @@ @Component public class SourcesConfig { - private boolean spotify = false; - private boolean appleMusic = false; - private boolean deezer = false; - private boolean yandexMusic = false; - - public boolean isSpotify() { - return this.spotify; - } - - public void setSpotify(boolean spotify) { - this.spotify = spotify; - } - - public boolean isAppleMusic() { - return this.appleMusic; - } - - public void setAppleMusic(boolean appleMusic) { - this.appleMusic = appleMusic; - } - - public boolean isDeezer() { - return this.deezer; - } - - public void setDeezer(boolean deezer) { - this.deezer = deezer; - } - - public boolean isYandexMusic() { - return this.yandexMusic; - } - - public void setYandexMusic(boolean yandexMusic) { - this.yandexMusic = yandexMusic; - } + private boolean spotify = false; + private boolean appleMusic = false; + private boolean deezer = false; + private boolean yandexMusic = false; + + public boolean isSpotify() { + return this.spotify; + } + + public void setSpotify(boolean spotify) { + this.spotify = spotify; + } + + public boolean isAppleMusic() { + return this.appleMusic; + } + + public void setAppleMusic(boolean appleMusic) { + this.appleMusic = appleMusic; + } + + public boolean isDeezer() { + return this.deezer; + } + + public void setDeezer(boolean deezer) { + this.deezer = deezer; + } + + public boolean isYandexMusic() { + return this.yandexMusic; + } + + public void setYandexMusic(boolean yandexMusic) { + this.yandexMusic = yandexMusic; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java index dba46bf0..d345d063 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/SpotifyConfig.java @@ -7,32 +7,32 @@ @Component public class SpotifyConfig { - private String clientId; - private String clientSecret; - private String countryCode; + private String clientId; + private String clientSecret; + private String countryCode; - public String getClientId() { - return this.clientId; - } + public String getClientId() { + return this.clientId; + } - public void setClientId(String clientId) { - this.clientId = clientId; - } + public void setClientId(String clientId) { + this.clientId = clientId; + } - public String getClientSecret() { - return this.clientSecret; - } + public String getClientSecret() { + return this.clientSecret; + } - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } - public String getCountryCode() { - return this.countryCode; - } + public String getCountryCode() { + return this.countryCode; + } - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java index d4fa37be..dd873f68 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/YandexMusicConfig.java @@ -7,13 +7,13 @@ @Component public class YandexMusicConfig { - private String accessToken; + private String accessToken; - public String getAccessToken() { - return this.accessToken; - } + public String getAccessToken() { + return this.accessToken; + } - public void setAccessToken(String accessToken) { - this.accessToken = accessToken; - } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } } diff --git a/plugin/src/main/resources/lavalink-plugins/lavasrc.properties b/plugin/src/main/resources/lavalink-plugins/lavasrc.properties index e6b2337b..0396e1e4 100644 --- a/plugin/src/main/resources/lavalink-plugins/lavasrc.properties +++ b/plugin/src/main/resources/lavalink-plugins/lavasrc.properties @@ -1,3 +1,3 @@ name=lavasrc path=com.github.topisenpai.lavasrc -version=3.1.6 +version=3.2.0 From a0a5a35b5184b52e2ae0ac8007d53de58eac75d6 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Sat, 17 Dec 2022 21:07:59 +0100 Subject: [PATCH 04/11] update to lavalink v4 with plugin info modification --- main/build.gradle | 3 +- .../applemusic/AppleMusicAudioTrack.java | 6 +- .../applemusic/AppleMusicSourceManager.java | 106 ++++++++++-------- .../deezer/DeezerAudioSourceManager.java | 31 +++-- .../lavasrc/deezer/DeezerAudioTrack.java | 16 +-- .../mirror/MirroringAudioSourceManager.java | 12 -- .../lavasrc/mirror/MirroringAudioTrack.java | 18 +-- .../lavasrc/spotify/SpotifyAudioTrack.java | 6 +- .../lavasrc/spotify/SpotifySourceManager.java | 26 +++-- plugin/build.gradle | 4 +- .../LavaSrcAudioPluginInfoModifier.java | 44 ++++++++ .../lavasrc/plugin/LavaSrcJsonAppender.java | 86 -------------- .../plugin/LavaSrcPluginDataAppender.java | 86 -------------- 13 files changed, 146 insertions(+), 298 deletions(-) create mode 100644 plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java delete mode 100644 plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java delete mode 100644 plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java diff --git a/main/build.gradle b/main/build.gradle index 02f1eba1..94202f71 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -6,9 +6,10 @@ plugins { var moduleName = "lavasrc" dependencies { - compileOnly "com.github.walkyst:lavaplayer-fork:1.3.99.1" + compileOnly "com.github.walkyst:lavaplayer-fork:e833a69" implementation "org.jsoup:jsoup:1.14.3" implementation "commons-io:commons-io:2.6" + compileOnly "org.slf4j:slf4j-api:1.7.25" } publishing { diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java index 380ad7f4..9d5cce5c 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioTrack.java @@ -6,13 +6,13 @@ public class AppleMusicAudioTrack extends MirroringAudioTrack { - public AppleMusicAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, AppleMusicSourceManager sourceManager) { - super(trackInfo, isrc, artworkURL, sourceManager); + public AppleMusicAudioTrack(AudioTrackInfo trackInfo, AppleMusicSourceManager sourceManager) { + super(trackInfo, sourceManager); } @Override protected AudioTrack makeShallowClone() { - return new AppleMusicAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (AppleMusicSourceManager) this.sourceManager); + return new AppleMusicAudioTrack(this.trackInfo, (AppleMusicSourceManager) this.sourceManager); } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java index 2b866cb3..df75cde0 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicSourceManager.java @@ -2,12 +2,15 @@ import com.github.topisenpai.lavasrc.mirror.MirroringAudioSourceManager; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import com.sedmelluq.discord.lavaplayer.track.*; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import com.sedmelluq.discord.lavaplayer.track.AudioReference; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; import org.apache.commons.io.IOUtils; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; @@ -17,6 +20,7 @@ import org.slf4j.LoggerFactory; import java.io.DataInput; +import java.io.DataOutput; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -30,17 +34,17 @@ public class AppleMusicSourceManager extends MirroringAudioSourceManager implements HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?[a-zA-Z]{2}/)?(?album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?[a-zA-Z\\d\\-.]+)(\\?i=(?\\d+))?"); - public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?(ey[\\w-]+)\\.([\\w-]+)\\.([\\w-]+))\""); - public static final String SEARCH_PREFIX = "amsearch:"; - public static final int MAX_PAGE_ITEMS = 300; - public static final String API_BASE = "https://api.music.apple.com/v1/"; - private static final Logger log = LoggerFactory.getLogger(AppleMusicSourceManager.class); - private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - private final String countryCode; - private String token; - private String origin; - private Instant tokenExpire; + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?[a-zA-Z]{2}/)?(?album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?[a-zA-Z\\d\\-.]+)(\\?i=(?\\d+))?"); + public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?(ey[\\w-]+)\\.([\\w-]+)\\.([\\w-]+))\""); + public static final String SEARCH_PREFIX = "amsearch:"; + public static final int MAX_PAGE_ITEMS = 300; + public static final String API_BASE = "https://api.music.apple.com/v1/"; + private static final Logger log = LoggerFactory.getLogger(AppleMusicSourceManager.class); + private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + private final String countryCode; + private String token; + private String origin; + private Instant tokenExpire; public AppleMusicSourceManager(String[] providers, String mediaAPIToken, String countryCode, AudioPlayerManager audioPlayerManager) { super(providers, audioPlayerManager); @@ -63,12 +67,18 @@ public String getSourceName() { } @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new AppleMusicAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) { + + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new AppleMusicAudioTrack(trackInfo, this); } @Override @@ -117,32 +127,32 @@ public void parseTokenData() throws IOException { this.origin = json.get("root_https_origin").index(0).text(); } - public void requestToken() throws IOException { - var request = new HttpGet("https://music.apple.com"); - try (var response = this.httpInterfaceManager.getInterface().execute(request)) { - var document = Jsoup.parse(response.getEntity().getContent(), null, ""); - var elements = document.select("script[type=module][src~=/assets/index.*.js]"); - if (elements.isEmpty()) { - throw new IllegalStateException("Cannot find token script element"); - } - - for (var element : elements) { - var tokenScriptURL = element.attr("src"); - request = new HttpGet("https://music.apple.com" + tokenScriptURL); - try (var indexResponse = this.httpInterfaceManager.getInterface().execute(request)) { - var tokenScript = IOUtils.toString(indexResponse.getEntity().getContent(), StandardCharsets.UTF_8); - var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript); - if (tokenMatcher.find()) { - this.token = tokenMatcher.group("token"); - this.parseTokenData(); - return; - } - } - - } - } - throw new IllegalStateException("Cannot find token script url"); - } + public void requestToken() throws IOException { + var request = new HttpGet("https://music.apple.com"); + try (var response = this.httpInterfaceManager.getInterface().execute(request)) { + var document = Jsoup.parse(response.getEntity().getContent(), null, ""); + var elements = document.select("script[type=module][src~=/assets/index.*.js]"); + if (elements.isEmpty()) { + throw new IllegalStateException("Cannot find token script element"); + } + + for (var element : elements) { + var tokenScriptURL = element.attr("src"); + request = new HttpGet("https://music.apple.com" + tokenScriptURL); + try (var indexResponse = this.httpInterfaceManager.getInterface().execute(request)) { + var tokenScript = IOUtils.toString(indexResponse.getEntity().getContent(), StandardCharsets.UTF_8); + var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript); + if (tokenMatcher.find()) { + this.token = tokenMatcher.group("token"); + this.parseTokenData(); + return; + } + } + + } + } + throw new IllegalStateException("Cannot find token script url"); + } public String getToken() throws IOException { if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) { @@ -258,10 +268,10 @@ private AudioTrack parseTrack(JsonBrowser json) { attributes.get("durationInMillis").asLong(0), json.get("id").text(), false, - attributes.get("url").text() + attributes.get("url").text(), + this.parseArtworkUrl(attributes.get("artwork")), + attributes.get("isrc").text() ), - attributes.get("isrc").text(), - this.parseArtworkUrl(attributes.get("artwork")), this ); } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java index 85777bf9..ff51d776 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioSourceManager.java @@ -2,7 +2,6 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; -import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; @@ -130,15 +129,17 @@ private List parseTracks(JsonBrowser json) { private AudioTrack parseTrack(JsonBrowser json) { var id = json.get("id").text(); - return new DeezerAudioTrack(new AudioTrackInfo( - json.get("title").text(), - json.get("artist").get("name").text(), - json.get("duration").as(Long.class) * 1000, - id, - false, - "https://deezer.com/track/" + id), - json.get("isrc").text(), - json.get("album").get("cover_xl").text(), + return new DeezerAudioTrack( + new AudioTrackInfo( + json.get("title").text(), + json.get("artist").get("name").text(), + json.get("duration").as(Long.class) * 1000, + id, + false, + "https://deezer.com/track/" + id, + json.get("album").get("cover_xl").text(), + json.get("isrc").text() + ), this ); } @@ -209,18 +210,12 @@ public boolean isTrackEncodable(AudioTrack track) { @Override public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var deezerAudioTrack = ((DeezerAudioTrack) track); - DataFormatTools.writeNullableText(output, deezerAudioTrack.getISRC()); - DataFormatTools.writeNullableText(output, deezerAudioTrack.getArtworkURL()); + } @Override public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new DeezerAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); + return new DeezerAudioTrack(trackInfo, this); } @Override diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java index 3402815e..ac93e572 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioTrack.java @@ -20,25 +20,13 @@ public class DeezerAudioTrack extends DelegatedAudioTrack { - private final String isrc; - private final String artworkURL; private final DeezerAudioSourceManager sourceManager; - public DeezerAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, DeezerAudioSourceManager sourceManager) { + public DeezerAudioTrack(AudioTrackInfo trackInfo, DeezerAudioSourceManager sourceManager) { super(trackInfo); - this.isrc = isrc; - this.artworkURL = artworkURL; this.sourceManager = sourceManager; } - public String getISRC() { - return this.isrc; - } - - public String getArtworkURL() { - return this.artworkURL; - } - 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 = HttpClientTools.fetchResponseAsJson(this.sourceManager.getHttpInterface(), getSessionID); @@ -95,7 +83,7 @@ public void process(LocalAudioTrackExecutor executor) throws Exception { @Override protected AudioTrack makeShallowClone() { - return new DeezerAudioTrack(this.trackInfo, this.isrc, this.artworkURL, this.sourceManager); + return new DeezerAudioTrack(this.trackInfo, this.sourceManager); } @Override diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java index ab90b4d2..c2d6903c 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioSourceManager.java @@ -33,16 +33,4 @@ public AudioPlayerManager getAudioPlayerManager() { return this.audioPlayerManager; } - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - var isrcAudioTrack = ((MirroringAudioTrack) track); - DataFormatTools.writeNullableText(output, isrcAudioTrack.getISRC()); - DataFormatTools.writeNullableText(output, isrcAudioTrack.getArtworkURL()); - } - } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java index 0b76c1cb..6a647fae 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java @@ -25,25 +25,13 @@ public abstract class MirroringAudioTrack extends DelegatedAudioTrack { private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); - protected final String isrc; - protected final String artworkURL; protected final MirroringAudioSourceManager sourceManager; - public MirroringAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, MirroringAudioSourceManager sourceManager) { + public MirroringAudioTrack(AudioTrackInfo trackInfo, MirroringAudioSourceManager sourceManager) { super(trackInfo); - this.isrc = isrc; - this.artworkURL = artworkURL; this.sourceManager = sourceManager; } - public String getISRC() { - return this.isrc; - } - - public String getArtworkURL() { - return this.artworkURL; - } - private String getTrackTitle() { var query = this.trackInfo.title; if (!this.trackInfo.author.equals("unknown")) { @@ -68,8 +56,8 @@ public void process(LocalAudioTrackExecutor executor) throws Exception { } if (provider.contains(ISRC_PATTERN)) { - if (this.isrc != null) { - provider = provider.replace(ISRC_PATTERN, this.isrc); + if (this.trackInfo.isrc != null) { + provider = provider.replace(ISRC_PATTERN, this.trackInfo.isrc); } else { log.debug("Ignoring identifier \"" + provider + "\" because this track does not have an ISRC!"); continue; diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java index db02863d..f77ea5f1 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioTrack.java @@ -6,13 +6,13 @@ public class SpotifyAudioTrack extends MirroringAudioTrack { - public SpotifyAudioTrack(AudioTrackInfo trackInfo, String isrc, String artworkURL, SpotifySourceManager sourceManager) { - super(trackInfo, isrc, artworkURL, sourceManager); + public SpotifyAudioTrack(AudioTrackInfo trackInfo, SpotifySourceManager sourceManager) { + super(trackInfo, sourceManager); } @Override protected AudioTrack makeShallowClone() { - return new SpotifyAudioTrack(this.trackInfo, this.isrc, this.artworkURL, (SpotifySourceManager) this.sourceManager); + return new SpotifyAudioTrack(this.trackInfo, (SpotifySourceManager) this.sourceManager); } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java index 6ab41cf9..0403bb58 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifySourceManager.java @@ -2,7 +2,6 @@ import com.github.topisenpai.lavasrc.mirror.MirroringAudioSourceManager; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; @@ -22,6 +21,7 @@ import org.slf4j.LoggerFactory; import java.io.DataInput; +import java.io.DataOutput; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -75,12 +75,18 @@ public String getSourceName() { } @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new SpotifyAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) { + + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new SpotifyAudioTrack(trackInfo, this); } @Override @@ -267,10 +273,10 @@ private AudioTrack parseTrack(JsonBrowser json) { json.get("duration_ms").asLong(0), json.get("id").text(), false, - json.get("external_urls").get("spotify").text() + json.get("external_urls").get("spotify").text(), + json.get("album").get("images").index(0).get("url").text(), + json.get("external_ids").get("isrc").text() ), - json.get("external_ids").get("isrc").text(), - json.get("album").get("images").index(0).get("url").text(), this ); } diff --git a/plugin/build.gradle b/plugin/build.gradle index 610b5884..802a5a13 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -9,8 +9,8 @@ var moduleName = "lavasrc-plugin" mainClassName = "org.springframework.boot.loader.JarLauncher" dependencies { - compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:f66611f") - runtimeOnly("com.github.freyacodes.lavalink:Lavalink-Server:3.6.0") + compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:2b7c6c7") + runtimeOnly("com.github.TopiSenpai.Lavalink:Lavalink-Server:2b7c6c7") implementation project(":main") } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java new file mode 100644 index 00000000..1c92c5f8 --- /dev/null +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java @@ -0,0 +1,44 @@ +package com.github.topisenpai.lavasrc.plugin; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioPlaylist; +import com.github.topisenpai.lavasrc.deezer.DeezerAudioPlaylist; +import com.github.topisenpai.lavasrc.spotify.SpotifyAudioPlaylist; +import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import dev.arbjerg.lavalink.api.AudioPluginInfoModifier; +import org.springframework.stereotype.Component; + +@Component +public class LavaSrcAudioPluginInfoModifier implements AudioPluginInfoModifier { + + @Override + public void modifyAudioPlaylistPluginInfo(AudioPlaylist playlist, ObjectNode node) { + if (playlist instanceof SpotifyAudioPlaylist) { + var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; + node.set("type", new TextNode(spotifyPlaylist.getType())); + node.set("identifier", new TextNode(spotifyPlaylist.getIdentifier())); + node.set("artworkURL", new TextNode(spotifyPlaylist.getArtworkURL())); + node.set("author", new TextNode(spotifyPlaylist.getAuthor())); + } else if (playlist instanceof AppleMusicAudioPlaylist) { + var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; + node.set("type", new TextNode(appleMusicPlaylist.getType())); + node.set("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); + node.set("artworkURL", new TextNode(appleMusicPlaylist.getArtworkURL())); + node.set("author", new TextNode(appleMusicPlaylist.getAuthor())); + } else if (playlist instanceof DeezerAudioPlaylist) { + var deezerPlaylist = (DeezerAudioPlaylist) playlist; + node.set("type", new TextNode(deezerPlaylist.getType())); + node.set("identifier", new TextNode(deezerPlaylist.getIdentifier())); + node.set("artworkURL", new TextNode(deezerPlaylist.getArtworkURL())); + node.set("author", new TextNode(deezerPlaylist.getAuthor())); + } else if (playlist instanceof YandexMusicAudioPlaylist) { + var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; + node.set("type", new TextNode(yandexMusicPlaylist.getType())); + node.set("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); + node.set("artworkURL", new TextNode(yandexMusicPlaylist.getArtworkURL())); + node.set("author", new TextNode(yandexMusicPlaylist.getAuthor())); + } + } +} diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java deleted file mode 100644 index 7d75a4bf..00000000 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcJsonAppender.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.topisenpai.lavasrc.plugin; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioPlaylist; -import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioTrack; -import com.github.topisenpai.lavasrc.deezer.DeezerAudioPlaylist; -import com.github.topisenpai.lavasrc.deezer.DeezerAudioTrack; -import com.github.topisenpai.lavasrc.spotify.SpotifyAudioPlaylist; -import com.github.topisenpai.lavasrc.spotify.SpotifyAudioTrack; -import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioPlaylist; -import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioTrack; -import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; -import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import dev.arbjerg.lavalink.api.AudioPlaylistJsonAppender; -import dev.arbjerg.lavalink.api.AudioTrackJsonAppender; -import org.springframework.stereotype.Component; - -import java.util.HashMap; -import java.util.Map; - -@Component -public class LavaSrcJsonAppender implements AudioTrackJsonAppender, AudioPlaylistJsonAppender { - - @Override - public Map appendFields(AudioPlaylist playlist) { - var map = new HashMap(); - - if (playlist instanceof SpotifyAudioPlaylist) { - var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; - map.put("type", new TextNode(spotifyPlaylist.getType())); - map.put("identifier", new TextNode(spotifyPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(spotifyPlaylist.getArtworkURL())); - map.put("author", new TextNode(spotifyPlaylist.getAuthor())); - - } else if (playlist instanceof AppleMusicAudioPlaylist) { - var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; - map.put("type", new TextNode(appleMusicPlaylist.getType())); - map.put("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(appleMusicPlaylist.getArtworkURL())); - map.put("author", new TextNode(appleMusicPlaylist.getAuthor())); - - } else if (playlist instanceof DeezerAudioPlaylist) { - var deezerPlaylist = (DeezerAudioPlaylist) playlist; - map.put("type", new TextNode(deezerPlaylist.getType())); - map.put("identifier", new TextNode(deezerPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(deezerPlaylist.getArtworkURL())); - map.put("author", new TextNode(deezerPlaylist.getAuthor())); - - } else if (playlist instanceof YandexMusicAudioPlaylist) { - var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; - map.put("type", new TextNode(yandexMusicPlaylist.getType())); - map.put("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(yandexMusicPlaylist.getArtworkURL())); - map.put("author", new TextNode(yandexMusicPlaylist.getAuthor())); - } - - return map; - } - - @Override - public Map appendFields(AudioTrack audioTrack) { - var map = new HashMap(); - - if (audioTrack instanceof SpotifyAudioTrack) { - var spotifyAudioTrack = (SpotifyAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(spotifyAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(spotifyAudioTrack.getISRC())); - - } else if (audioTrack instanceof AppleMusicAudioTrack) { - var appleMusicAudioTrack = (AppleMusicAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(appleMusicAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(appleMusicAudioTrack.getISRC())); - - } else if (audioTrack instanceof DeezerAudioTrack) { - var deezerAudioTrack = (DeezerAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(deezerAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(deezerAudioTrack.getISRC())); - } else if (audioTrack instanceof YandexMusicAudioTrack) { - var yandexMusicAudioTrack = (YandexMusicAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(yandexMusicAudioTrack.getArtworkURL())); - } - - return map; - } -} diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java deleted file mode 100644 index 23a6ae7d..00000000 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcPluginDataAppender.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.topisenpai.lavasrc.plugin; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioPlaylist; -import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioTrack; -import com.github.topisenpai.lavasrc.deezer.DeezerAudioPlaylist; -import com.github.topisenpai.lavasrc.deezer.DeezerAudioTrack; -import com.github.topisenpai.lavasrc.spotify.SpotifyAudioPlaylist; -import com.github.topisenpai.lavasrc.spotify.SpotifyAudioTrack; -import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioPlaylist; -import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioTrack; -import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; -import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import dev.arbjerg.lavalink.api.JsonPluginDataAppender; -import org.springframework.stereotype.Component; - -import java.util.HashMap; -import java.util.Map; - -@Component -public class LavaSrcPluginDataAppender implements JsonPluginDataAppender { - - @Override - public Map addTrackPluginData(AudioTrack audioTrack) { - var map = new HashMap(); - - if (audioTrack instanceof SpotifyAudioTrack) { - var spotifyAudioTrack = (SpotifyAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(spotifyAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(spotifyAudioTrack.getISRC())); - - } else if (audioTrack instanceof AppleMusicAudioTrack) { - var appleMusicAudioTrack = (AppleMusicAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(appleMusicAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(appleMusicAudioTrack.getISRC())); - - } else if (audioTrack instanceof DeezerAudioTrack) { - var deezerAudioTrack = (DeezerAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(deezerAudioTrack.getArtworkURL())); - map.put("isrc", new TextNode(deezerAudioTrack.getISRC())); - } else if (audioTrack instanceof YandexMusicAudioTrack) { - var yandexMusicAudioTrack = (YandexMusicAudioTrack) audioTrack; - map.put("artworkURL", new TextNode(yandexMusicAudioTrack.getArtworkURL())); - } - - return map; - } - - @Override - public Map addPlaylistPluginData(AudioPlaylist playlist) { - var map = new HashMap(); - - if (playlist instanceof SpotifyAudioPlaylist) { - var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; - map.put("type", new TextNode(spotifyPlaylist.getType())); - map.put("identifier", new TextNode(spotifyPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(spotifyPlaylist.getArtworkURL())); - map.put("author", new TextNode(spotifyPlaylist.getAuthor())); - - } else if (playlist instanceof AppleMusicAudioPlaylist) { - var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; - map.put("type", new TextNode(appleMusicPlaylist.getType())); - map.put("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(appleMusicPlaylist.getArtworkURL())); - map.put("author", new TextNode(appleMusicPlaylist.getAuthor())); - - } else if (playlist instanceof DeezerAudioPlaylist) { - var deezerPlaylist = (DeezerAudioPlaylist) playlist; - map.put("type", new TextNode(deezerPlaylist.getType())); - map.put("identifier", new TextNode(deezerPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(deezerPlaylist.getArtworkURL())); - map.put("author", new TextNode(deezerPlaylist.getAuthor())); - - } else if (playlist instanceof YandexMusicAudioPlaylist) { - var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; - map.put("type", new TextNode(yandexMusicPlaylist.getType())); - map.put("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); - map.put("artworkURL", new TextNode(yandexMusicPlaylist.getArtworkURL())); - map.put("author", new TextNode(yandexMusicPlaylist.getAuthor())); - } - - return map; - } - -} From 7bd20fd36213eb684a4c2e74bbc7562e762af08e Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Fri, 6 Jan 2023 15:30:56 +0100 Subject: [PATCH 05/11] cleanup playlist stuff --- .../lavasrc/ExtendedAudioPlaylist.java | 38 +++++++++++++++++++ .../applemusic/AppleMusicAudioPlaylist.java | 31 ++------------- .../lavasrc/deezer/DeezerAudioPlaylist.java | 31 ++------------- .../lavasrc/spotify/SpotifyAudioPlaylist.java | 31 ++------------- .../yandexmusic/YandexMusicAudioPlaylist.java | 31 ++------------- 5 files changed, 50 insertions(+), 112 deletions(-) create mode 100644 main/src/main/java/com/github/topisenpai/lavasrc/ExtendedAudioPlaylist.java diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/ExtendedAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/ExtendedAudioPlaylist.java new file mode 100644 index 00000000..f83c6c31 --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/ExtendedAudioPlaylist.java @@ -0,0 +1,38 @@ +package com.github.topisenpai.lavasrc; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; + +import java.util.List; + +public class ExtendedAudioPlaylist extends BasicAudioPlaylist { + private final String type; + private final String identifier; + private final String artworkURL; + private final String author; + + public ExtendedAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, null, false); + this.type = type; + this.identifier = identifier; + this.artworkURL = artworkURL; + this.author = author; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return this.identifier; + } + + public String getArtworkURL() { + return this.artworkURL; + } + + public String getAuthor() { + return this.author; + } + +} diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java index 70166218..462461fd 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java @@ -1,39 +1,14 @@ package com.github.topisenpai.lavasrc.applemusic; +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; import java.util.List; -public class AppleMusicAudioPlaylist extends BasicAudioPlaylist { - - private final String type; - private final String identifier; - private final String artworkURL; - private final String author; +public class AppleMusicAudioPlaylist extends ExtendedAudioPlaylist { public AppleMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { - super(name, tracks, null, false); - this.type = type; - this.identifier = identifier; - this.artworkURL = artworkURL; - this.author = author; - } - - public String getType() { - return type; - } - - public String getIdentifier() { - return this.identifier; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - public String getAuthor() { - return this.author; + super(name, tracks, type, identifier, artworkURL, author); } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java index 901a448e..3f9f80c7 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java @@ -1,39 +1,14 @@ package com.github.topisenpai.lavasrc.deezer; +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; import java.util.List; -public class DeezerAudioPlaylist extends BasicAudioPlaylist { - - private final String type; - private final String identifier; - private final String artworkURL; - private final String author; +public class DeezerAudioPlaylist extends ExtendedAudioPlaylist { public DeezerAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { - super(name, tracks, null, false); - this.type = type; - this.identifier = identifier; - this.artworkURL = artworkURL; - this.author = author; - } - - public String getType() { - return type; - } - - public String getIdentifier() { - return this.identifier; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - public String getAuthor() { - return this.author; + super(name, tracks, type, identifier, artworkURL, author); } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java index 39bc5091..65816e3b 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java @@ -1,39 +1,14 @@ package com.github.topisenpai.lavasrc.spotify; +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; import java.util.List; -public class SpotifyAudioPlaylist extends BasicAudioPlaylist { - - private final String type; - private final String identifier; - private final String artworkURL; - private final String author; +public class SpotifyAudioPlaylist extends ExtendedAudioPlaylist { public SpotifyAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { - super(name, tracks, null, false); - this.type = type; - this.identifier = identifier; - this.artworkURL = artworkURL; - this.author = author; - } - - public String getType() { - return type; - } - - public String getIdentifier() { - return this.identifier; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - public String getAuthor() { - return this.author; + super(name, tracks, type, identifier, artworkURL, author); } } diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java index c461018b..54f1246c 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java @@ -1,39 +1,14 @@ package com.github.topisenpai.lavasrc.yandexmusic; +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; import java.util.List; -public class YandexMusicAudioPlaylist extends BasicAudioPlaylist { - - private final String type; - private final String identifier; - private final String artworkURL; - private final String author; +public class YandexMusicAudioPlaylist extends ExtendedAudioPlaylist { public YandexMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { - super(name, tracks, null, false); - this.type = type; - this.identifier = identifier; - this.artworkURL = artworkURL; - this.author = author; - } - - public String getType() { - return type; - } - - public String getIdentifier() { - return this.identifier; - } - - public String getArtworkURL() { - return this.artworkURL; - } - - public String getAuthor() { - return this.author; + super(name, tracks, type, identifier, artworkURL, author); } } From a4a67009f589f3ebbce63724e8e5ecd089fc2ead Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Sun, 15 Jan 2023 00:02:23 +0100 Subject: [PATCH 06/11] rename artworkURL -> artworkUrl --- .../lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java index 1c92c5f8..131e2418 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java @@ -19,25 +19,25 @@ public void modifyAudioPlaylistPluginInfo(AudioPlaylist playlist, ObjectNode nod var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; node.set("type", new TextNode(spotifyPlaylist.getType())); node.set("identifier", new TextNode(spotifyPlaylist.getIdentifier())); - node.set("artworkURL", new TextNode(spotifyPlaylist.getArtworkURL())); + node.set("artworkUrl", new TextNode(spotifyPlaylist.getArtworkURL())); node.set("author", new TextNode(spotifyPlaylist.getAuthor())); } else if (playlist instanceof AppleMusicAudioPlaylist) { var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; node.set("type", new TextNode(appleMusicPlaylist.getType())); node.set("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); - node.set("artworkURL", new TextNode(appleMusicPlaylist.getArtworkURL())); + node.set("artworkUrl", new TextNode(appleMusicPlaylist.getArtworkURL())); node.set("author", new TextNode(appleMusicPlaylist.getAuthor())); } else if (playlist instanceof DeezerAudioPlaylist) { var deezerPlaylist = (DeezerAudioPlaylist) playlist; node.set("type", new TextNode(deezerPlaylist.getType())); node.set("identifier", new TextNode(deezerPlaylist.getIdentifier())); - node.set("artworkURL", new TextNode(deezerPlaylist.getArtworkURL())); + node.set("artworkUrl", new TextNode(deezerPlaylist.getArtworkURL())); node.set("author", new TextNode(deezerPlaylist.getAuthor())); } else if (playlist instanceof YandexMusicAudioPlaylist) { var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; node.set("type", new TextNode(yandexMusicPlaylist.getType())); node.set("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); - node.set("artworkURL", new TextNode(yandexMusicPlaylist.getArtworkURL())); + node.set("artworkUrl", new TextNode(yandexMusicPlaylist.getArtworkURL())); node.set("author", new TextNode(yandexMusicPlaylist.getAuthor())); } } From 538cf8fe9cf7fa0c9dd0241c5a027b4a16588304 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Thu, 2 Feb 2023 20:40:55 +0100 Subject: [PATCH 07/11] update to lavalink repo --- plugin/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/build.gradle b/plugin/build.gradle index 802a5a13..1780df0a 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -9,8 +9,8 @@ var moduleName = "lavasrc-plugin" mainClassName = "org.springframework.boot.loader.JarLauncher" dependencies { - compileOnly("com.github.TopiSenpai.Lavalink:plugin-api:2b7c6c7") - runtimeOnly("com.github.TopiSenpai.Lavalink:Lavalink-Server:2b7c6c7") + compileOnly("com.github.freyacodes.Lavalink:plugin-api:4cfdc68") + // runtimeOnly("com.github.freyacodes.Lavalink:Lavalink-Server:4cfdc68") implementation project(":main") } From a7f5703fe5f8b6acf096acac1a56ae6157966ed6 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Sun, 30 Apr 2023 16:39:40 +0200 Subject: [PATCH 08/11] bleh --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index f1a3f703..5240d16a 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -3,7 +3,7 @@ plugins { } dependencies { - compileOnly "com.github.walkyst:lavaplayer-fork:e833a69" + compileOnly "com.github.walkyst:lavaplayer-fork:ef075855da" implementation "org.jsoup:jsoup:1.14.3" implementation "commons-io:commons-io:2.6" compileOnly "org.slf4j:slf4j-api:1.7.25" From f1bc02e56ac532065f51fafc829f3be70cdd1760 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Sun, 30 Apr 2023 16:56:06 +0200 Subject: [PATCH 09/11] fix plugin api version --- plugin/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/build.gradle b/plugin/build.gradle index a482aa22..c022ad33 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -8,8 +8,8 @@ plugins { mainClassName = "org.springframework.boot.loader.JarLauncher" dependencies { - compileOnly("com.github.freyacodes.Lavalink:plugin-api:4cfdc68") - // runtimeOnly("com.github.freyacodes.Lavalink:Lavalink-Server:4cfdc68") + compileOnly("com.github.freyacodes.Lavalink:plugin-api:43edfbc8b5") + // runtimeOnly("com.github.freyacodes.Lavalink:Lavalink-Server:43edfbc8b5") implementation project(":main") } From d81d18f92c5e4f8d86a922a1ac88a0caff054f57 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Thu, 25 May 2023 13:51:59 +0200 Subject: [PATCH 10/11] update to latest lavalink --- plugin/build.gradle | 4 +- .../LavaSrcAudioPluginInfoModifier.java | 54 +++++++------------ 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/plugin/build.gradle b/plugin/build.gradle index 30680cd4..43eaa757 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -9,8 +9,8 @@ mainClassName = "org.springframework.boot.loader.JarLauncher" archivesBaseName = "lavasrc-plugin" dependencies { - compileOnly("com.github.freyacodes.Lavalink:plugin-api:43edfbc8b5") - // runtimeOnly("com.github.freyacodes.Lavalink:Lavalink-Server:43edfbc8b5") + compileOnly("com.github.lavalink-devs.Lavalink:plugin-api:513acb0557") + runtimeOnly("com.github.lavalink-devs.Lavalink:Lavalink-Server:513acb0557") implementation project(":main") } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java index 131e2418..5f76e540 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java @@ -1,44 +1,30 @@ package com.github.topisenpai.lavasrc.plugin; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.github.topisenpai.lavasrc.applemusic.AppleMusicAudioPlaylist; -import com.github.topisenpai.lavasrc.deezer.DeezerAudioPlaylist; -import com.github.topisenpai.lavasrc.spotify.SpotifyAudioPlaylist; -import com.github.topisenpai.lavasrc.yandexmusic.YandexMusicAudioPlaylist; +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import dev.arbjerg.lavalink.api.AudioPluginInfoModifier; +import kotlinx.serialization.json.JsonElementKt; +import kotlinx.serialization.json.JsonObject; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; +import java.util.Map; + @Component public class LavaSrcAudioPluginInfoModifier implements AudioPluginInfoModifier { - @Override - public void modifyAudioPlaylistPluginInfo(AudioPlaylist playlist, ObjectNode node) { - if (playlist instanceof SpotifyAudioPlaylist) { - var spotifyPlaylist = (SpotifyAudioPlaylist) playlist; - node.set("type", new TextNode(spotifyPlaylist.getType())); - node.set("identifier", new TextNode(spotifyPlaylist.getIdentifier())); - node.set("artworkUrl", new TextNode(spotifyPlaylist.getArtworkURL())); - node.set("author", new TextNode(spotifyPlaylist.getAuthor())); - } else if (playlist instanceof AppleMusicAudioPlaylist) { - var appleMusicPlaylist = (AppleMusicAudioPlaylist) playlist; - node.set("type", new TextNode(appleMusicPlaylist.getType())); - node.set("identifier", new TextNode(appleMusicPlaylist.getIdentifier())); - node.set("artworkUrl", new TextNode(appleMusicPlaylist.getArtworkURL())); - node.set("author", new TextNode(appleMusicPlaylist.getAuthor())); - } else if (playlist instanceof DeezerAudioPlaylist) { - var deezerPlaylist = (DeezerAudioPlaylist) playlist; - node.set("type", new TextNode(deezerPlaylist.getType())); - node.set("identifier", new TextNode(deezerPlaylist.getIdentifier())); - node.set("artworkUrl", new TextNode(deezerPlaylist.getArtworkURL())); - node.set("author", new TextNode(deezerPlaylist.getAuthor())); - } else if (playlist instanceof YandexMusicAudioPlaylist) { - var yandexMusicPlaylist = (YandexMusicAudioPlaylist) playlist; - node.set("type", new TextNode(yandexMusicPlaylist.getType())); - node.set("identifier", new TextNode(yandexMusicPlaylist.getIdentifier())); - node.set("artworkUrl", new TextNode(yandexMusicPlaylist.getArtworkURL())); - node.set("author", new TextNode(yandexMusicPlaylist.getAuthor())); - } - } + @Override + public JsonObject modifyAudioPlaylistPluginInfo(@NotNull AudioPlaylist playlist) { + if (playlist instanceof ExtendedAudioPlaylist) { + var extendedPlaylist = (ExtendedAudioPlaylist) playlist; + + return new JsonObject(Map.of( + "type", JsonElementKt.JsonPrimitive(extendedPlaylist.getType()), + "identifier", JsonElementKt.JsonPrimitive(extendedPlaylist.getIdentifier()), + "artworkUrl", JsonElementKt.JsonPrimitive(extendedPlaylist.getArtworkURL()), + "author", JsonElementKt.JsonPrimitive(extendedPlaylist.getAuthor()) + )); + } + return null; + } } From 5afe4eb9962b27dea802e7bd03b6181607603d71 Mon Sep 17 00:00:00 2001 From: TopiSenpai Date: Thu, 25 May 2023 17:48:47 +0200 Subject: [PATCH 11/11] add delegate track into track json --- main/build.gradle | 1 + .../lavasrc/mirror/MirroringAudioTrack.java | 8 ++++ plugin/build.gradle | 7 +++- .../LavaSrcAudioPluginInfoModifier.java | 41 +++++++++++++++++-- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index fc57421a..63077f5d 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -5,6 +5,7 @@ plugins { archivesBaseName = "lavasrc" dependencies { + implementation 'org.jetbrains:annotations:24.0.0' compileOnly "com.github.walkyst:lavaplayer-fork:ef075855da" implementation "org.jsoup:jsoup:1.14.3" implementation "commons-io:commons-io:2.6" diff --git a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java index 0c4cbbad..15673d7e 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/MirroringAudioTrack.java @@ -11,6 +11,7 @@ import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,12 +22,18 @@ public abstract class MirroringAudioTrack extends DelegatedAudioTrack { private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); protected final MirroringAudioSourceManager sourceManager; + private AudioTrack delegate; public MirroringAudioTrack(AudioTrackInfo trackInfo, MirroringAudioSourceManager sourceManager) { super(trackInfo); this.sourceManager = sourceManager; } + @Nullable + public AudioTrack getDelegate() { + return this.delegate; + } + @Override public void process(LocalAudioTrackExecutor executor) throws Exception { AudioItem track = this.sourceManager.getResolver().apply(this); @@ -35,6 +42,7 @@ public void process(LocalAudioTrackExecutor executor) throws Exception { track = ((AudioPlaylist) track).getTracks().get(0); } if (track instanceof InternalAudioTrack) { + this.delegate = (AudioTrack) track; processDelegate((InternalAudioTrack) track, executor); return; } diff --git a/plugin/build.gradle b/plugin/build.gradle index 43eaa757..8492208a 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -9,8 +9,11 @@ mainClassName = "org.springframework.boot.loader.JarLauncher" archivesBaseName = "lavasrc-plugin" dependencies { - compileOnly("com.github.lavalink-devs.Lavalink:plugin-api:513acb0557") - runtimeOnly("com.github.lavalink-devs.Lavalink:Lavalink-Server:513acb0557") + compileOnly("com.github.lavalink-devs.Lavalink:plugin-api:09240339ce") + implementation("com.github.lavalink-devs.Lavalink:Lavalink-Server:09240339ce") { + exclude group: 'org.slf4j' + } + runtimeOnly("ch.qos.logback:logback-classic:1.2.3") implementation project(":main") } diff --git a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java index 5f76e540..2d26440c 100644 --- a/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java @@ -1,30 +1,63 @@ package com.github.topisenpai.lavasrc.plugin; import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; +import com.github.topisenpai.lavasrc.mirror.MirroringAudioTrack; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import dev.arbjerg.lavalink.api.AudioPluginInfoModifier; +import dev.arbjerg.lavalink.protocol.v4.Mapper; +import dev.arbjerg.lavalink.protocol.v4.Track; import kotlinx.serialization.json.JsonElementKt; import kotlinx.serialization.json.JsonObject; +import lavalink.server.util.UtilKt; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Map; @Component public class LavaSrcAudioPluginInfoModifier implements AudioPluginInfoModifier { + private final AudioPlayerManager playerManager; + private final List pluginInfoModifiers; + + public LavaSrcAudioPluginInfoModifier(AudioPlayerManager playerManager, List pluginInfoModifiers) { + this.playerManager = playerManager; + this.pluginInfoModifiers = pluginInfoModifiers; + } + + public JsonObject modifyAudioTrackPluginInfo(@NotNull AudioTrack track) { + if (track instanceof MirroringAudioTrack) { + var mirroringTrack = (MirroringAudioTrack) track; + var delegate = mirroringTrack.getDelegate(); + if (delegate == null) { + return null; + } + + return new JsonObject(Map.of( + "resolvedTrack", Mapper.getJson().encodeToJsonElement(Track.Companion.serializer(), UtilKt.toTrack(delegate, playerManager, pluginInfoModifiers)) + )); + } + return new JsonObject(Map.of( + "test", JsonElementKt.JsonPrimitive("lol") + )); + } + @Override public JsonObject modifyAudioPlaylistPluginInfo(@NotNull AudioPlaylist playlist) { if (playlist instanceof ExtendedAudioPlaylist) { var extendedPlaylist = (ExtendedAudioPlaylist) playlist; return new JsonObject(Map.of( - "type", JsonElementKt.JsonPrimitive(extendedPlaylist.getType()), - "identifier", JsonElementKt.JsonPrimitive(extendedPlaylist.getIdentifier()), - "artworkUrl", JsonElementKt.JsonPrimitive(extendedPlaylist.getArtworkURL()), - "author", JsonElementKt.JsonPrimitive(extendedPlaylist.getAuthor()) + "type", JsonElementKt.JsonPrimitive(extendedPlaylist.getType()), + "identifier", JsonElementKt.JsonPrimitive(extendedPlaylist.getIdentifier()), + "artworkUrl", JsonElementKt.JsonPrimitive(extendedPlaylist.getArtworkURL()), + "author", JsonElementKt.JsonPrimitive(extendedPlaylist.getAuthor()) )); } return null; } + }