From 3cf9a8c8d5862c0fc065a89478aa2391afec1f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?To=CF=80?= Date: Tue, 11 Jul 2023 00:56:50 +0200 Subject: [PATCH] add album name. artist artwork url & preview url to track & play preview url Co-authored-by: Michael Rittmeister --- main/build.gradle | 2 +- .../lavasrc/ExtendedAudioPlaylist.java | 8 +- .../lavasrc/ExtendedAudioSourceManager.java | 52 ++++++++ .../topi314/lavasrc/ExtendedAudioTrack.java | 31 +++++ .../applemusic/AppleMusicAudioTrack.java | 15 ++- .../applemusic/AppleMusicSourceManager.java | 115 ++++++++++-------- .../deezer/DeezerAudioSourceManager.java | 111 +++++++++-------- .../lavasrc/deezer/DeezerAudioTrack.java | 12 +- .../mirror/MirroringAudioSourceManager.java | 44 ++++++- .../lavasrc/mirror/MirroringAudioTrack.java | 22 +++- .../lavasrc/spotify/SpotifyAudioTrack.java | 17 ++- .../lavasrc/spotify/SpotifySourceManager.java | 83 +++++++------ .../LavaSrcAudioPluginInfoModifier.java | 16 +++ 13 files changed, 375 insertions(+), 153 deletions(-) create mode 100644 main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioSourceManager.java create mode 100644 main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioTrack.java diff --git a/main/build.gradle b/main/build.gradle index 81f51898..c5dd882b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -9,7 +9,7 @@ java { } dependencies { - compileOnly "com.github.walkyst:lavaplayer-fork:17c75f5" + compileOnly "com.github.topi314:lavaplayer-fork:8ca9418be9" implementation "org.jsoup:jsoup:1.14.3" implementation "commons-io:commons-io:2.6" compileOnly "org.slf4j:slf4j-api:1.7.25" diff --git a/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioPlaylist.java b/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioPlaylist.java index 4cd26010..552d3af5 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioPlaylist.java +++ b/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioPlaylist.java @@ -6,10 +6,10 @@ import java.util.List; public class ExtendedAudioPlaylist extends BasicAudioPlaylist { - private final String type; - private final String url; - private final String artworkURL; - private final String author; + protected final String type; + protected final String url; + protected final String artworkURL; + protected final String author; public ExtendedAudioPlaylist(String name, List tracks, String type, String url, String artworkURL, String author) { super(name, tracks, null, false); diff --git a/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioSourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioSourceManager.java new file mode 100644 index 00000000..d0dd711c --- /dev/null +++ b/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioSourceManager.java @@ -0,0 +1,52 @@ +package com.github.topi314.lavasrc; + +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; +import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.IOException; + +public abstract class ExtendedAudioSourceManager implements AudioSourceManager { + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + var extendedTrack = (ExtendedAudioTrack) track; + DataFormatTools.writeNullableText(output, extendedTrack.getAlbumName()); + DataFormatTools.writeNullableText(output, extendedTrack.getArtistArtworkUrl()); + DataFormatTools.writeNullableText(output, extendedTrack.getPreviewUrl()); + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + protected ExtendedAudioTrackInfo decodeTrack(DataInput input) throws IOException { + String albumName = null; + String artistArtworkUrl = null; + String previewUrl = null; + // Check if the input has more than 8 bytes available, which would indicate that the preview field is present. + // This is done to avoid breaking backwards compatibility with tracks that were saved before the preview field was added. + if (((DataInputStream) input).available() > Long.BYTES) { + albumName = DataFormatTools.readNullableText(input); + artistArtworkUrl = DataFormatTools.readNullableText(input); + previewUrl = DataFormatTools.readNullableText(input); + } + return new ExtendedAudioTrackInfo(albumName, artistArtworkUrl, previewUrl); + } + + protected static class ExtendedAudioTrackInfo { + public final String albumName; + public final String artistArtworkUrl; + public final String previewUrl; + + public ExtendedAudioTrackInfo(String albumName, String artistArtworkUrl, String previewUrl) { + this.albumName = albumName; + this.artistArtworkUrl = artistArtworkUrl; + this.previewUrl = previewUrl; + } + } +} diff --git a/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioTrack.java b/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioTrack.java new file mode 100644 index 00000000..05d66868 --- /dev/null +++ b/main/src/main/java/com/github/topi314/lavasrc/ExtendedAudioTrack.java @@ -0,0 +1,31 @@ +package com.github.topi314.lavasrc; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; + +public abstract class ExtendedAudioTrack extends DelegatedAudioTrack { + + protected final String albumName; + protected final String artistArtworkUrl; + protected final String previewUrl; + + public ExtendedAudioTrack(AudioTrackInfo trackInfo, String albumName, String artistArtworkUrl, String previewUrl) { + super(trackInfo); + this.albumName = albumName; + this.artistArtworkUrl = artistArtworkUrl; + this.previewUrl = previewUrl; + } + + public String getAlbumName() { + return this.albumName; + } + + public String getArtistArtworkUrl() { + return this.artistArtworkUrl; + } + + public String getPreviewUrl() { + return this.previewUrl; + } + +} diff --git a/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicAudioTrack.java b/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicAudioTrack.java index dc7862bc..b445117e 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicAudioTrack.java +++ b/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicAudioTrack.java @@ -1,13 +1,26 @@ package com.github.topi314.lavasrc.applemusic; +import com.github.topi314.lavasrc.mirror.MirroringAudioSourceManager; import com.github.topi314.lavasrc.mirror.MirroringAudioTrack; +import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegAudioTrack; +import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; public class AppleMusicAudioTrack extends MirroringAudioTrack { public AppleMusicAudioTrack(AudioTrackInfo trackInfo, AppleMusicSourceManager sourceManager) { - super(trackInfo, sourceManager); + super(trackInfo, null, null, null, sourceManager); + } + + public AppleMusicAudioTrack(AudioTrackInfo trackInfo, String albumName, String artistArtworkUrl, String previewUrl, MirroringAudioSourceManager sourceManager) { + super(trackInfo, albumName, artistArtworkUrl, previewUrl, sourceManager); + } + + @Override + protected InternalAudioTrack createAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream stream) { + return new MpegAudioTrack(trackInfo, stream); } @Override diff --git a/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicSourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicSourceManager.java index 1e70872d..f75e5e9d 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicSourceManager.java +++ b/main/src/main/java/com/github/topi314/lavasrc/applemusic/AppleMusicSourceManager.java @@ -23,22 +23,20 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; +import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; +import java.util.stream.Collectors; -public class AppleMusicSourceManager extends MirroringAudioSourceManager implements HttpConfigurable { +public class AppleMusicSourceManager extends MirroringAudioSourceManager { 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 String PREVIEW_PREFIX = "amprev:"; 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 int playlistPageLimit; private int albumPageLimit; @@ -94,12 +92,19 @@ public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { @Override public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + var identifier = reference.identifier; + var preview = reference.identifier.startsWith(PREVIEW_PREFIX); + + return loadItem(preview ? identifier.substring(PREVIEW_PREFIX.length()) : identifier, preview); + } + + public AudioItem loadItem(String identifier, boolean preview) { try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); + if (identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(identifier.substring(SEARCH_PREFIX.length()).trim(), preview); } - var matcher = URL_PATTERN.matcher(reference.identifier); + var matcher = URL_PATTERN.matcher(identifier); if (!matcher.find()) { return null; } @@ -108,20 +113,20 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) var id = matcher.group("identifier"); switch (matcher.group("type")) { case "song": - return this.getSong(id, countryCode); + return this.getSong(id, countryCode, preview); case "album": var id2 = matcher.group("identifier2"); if (id2 == null || id2.isEmpty()) { - return this.getAlbum(id, countryCode); + return this.getAlbum(id, countryCode, preview); } - return this.getSong(id2, countryCode); + return this.getSong(id2, countryCode, preview); case "playlist": - return this.getPlaylist(id, countryCode); + return this.getPlaylist(id, countryCode, preview); case "artist": - return this.getArtist(id, countryCode); + return this.getArtist(id, countryCode, preview); } } catch (IOException e) { throw new RuntimeException(e); @@ -181,21 +186,35 @@ public JsonBrowser getJson(String uri) throws IOException { 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); + public Map getArtistCover(List ids) throws IOException { + var json = getJson(API_BASE + "catalog/" + countryCode + "/artists?ids=" + String.join(",", ids)); + var data = json.get("data"); + + var output = new HashMap(ids.size()); + for (var i = 0; i < ids.size(); i++) { + var artist = data.index(i); + var artwork = artist.get("attributes").get("artwork"); + output.put(artist.get("id").text(), parseArtworkUrl(artwork)); + } + + return output; + } + + public AudioItem getSearch(String query, boolean preview) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/search?term=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&limit=" + 25 + "&extend=artistUrl"); if (json == null || json.get("results").get("songs").get("data").values().isEmpty()) { return AudioReference.NO_TRACK; } - return new BasicAudioPlaylist("Apple Music Search: " + query, this.parseTracks(json.get("results").get("songs")), null, true); + return new BasicAudioPlaylist("Apple Music Search: " + query, this.parseTracks(json.get("results").get("songs"), preview), null, true); } - public AudioItem getAlbum(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id); + public AudioItem getAlbum(String id, String countryCode, boolean preview) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id + "&extend=artistUrl"); if (json == null) { return AudioReference.NO_TRACK; } - var tracks = new ArrayList(); + var tracksRaw = JsonBrowser.newList(); JsonBrowser page; var offset = 0; var pages = 0; @@ -203,10 +222,11 @@ public AudioItem getAlbum(String id, String countryCode) throws IOException { page = this.getJson(API_BASE + "catalog/" + countryCode + "/albums/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); offset += MAX_PAGE_ITEMS; - tracks.addAll(this.parseTracks(page)); + page.values().forEach(tracksRaw::add); } while (page.get("next").text() != null && ++pages < albumPageLimit); + var tracks = parseTracks(tracksRaw, preview); if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } @@ -216,13 +236,13 @@ public AudioItem getAlbum(String id, String countryCode) throws IOException { return new AppleMusicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, "album", json.get("data").index(0).get("attributes").get("url").text(), artworkUrl, author); } - public AudioItem getPlaylist(String id, String countryCode) throws IOException { - var json = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id); + public AudioItem getPlaylist(String id, String countryCode, boolean preview) throws IOException { + var json = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id + "&extend=artistUrl"); if (json == null) { return AudioReference.NO_TRACK; } - var tracks = new ArrayList(); + var tracksRaw = JsonBrowser.newList(); JsonBrowser page; var offset = 0; var pages = 0; @@ -230,10 +250,11 @@ public AudioItem getPlaylist(String id, String countryCode) throws IOException { page = this.getJson(API_BASE + "catalog/" + countryCode + "/playlists/" + id + "/tracks?limit=" + MAX_PAGE_ITEMS + "&offset=" + offset); offset += MAX_PAGE_ITEMS; - tracks.addAll(parseTracks(page)); + page.values().forEach(tracksRaw::add); } while (page.get("next").text() != null && ++pages < playlistPageLimit); + var tracks = parseTracks(tracksRaw, preview); if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } @@ -243,7 +264,7 @@ public AudioItem getPlaylist(String id, String countryCode) throws IOException { return new AppleMusicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, "playlist", json.get("data").index(0).get("attributes").get("url").text(), artworkUrl, author); } - public AudioItem getArtist(String id, String countryCode) throws IOException { + public AudioItem getArtist(String id, String countryCode, boolean preview) 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; @@ -253,26 +274,33 @@ public AudioItem getArtist(String id, String countryCode) throws IOException { 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", json.get("data").index(0).get("attributes").get("url").text(), artworkUrl, author); + var artistArtwork = Map.of(jsonArtist.get("id").text(), artworkUrl); + return new AppleMusicAudioPlaylist(author + "'s Top Tracks", parseTracks(json, preview, artistArtwork), "artist", json.get("data").index(0).get("attributes").get("url").text(), artworkUrl, author); } - public AudioItem getSong(String id, String countryCode) throws IOException { + public AudioItem getSong(String id, String countryCode, boolean preview) 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)); + var artistArtwork = getArtistCover(List.of(parseArtistId(json))).values().iterator().next(); + return parseTrack(json.get("data").index(0), preview, artistArtwork); } - private List parseTracks(JsonBrowser json) { + private List parseTracks(JsonBrowser json, boolean preview, Map artistArtwork) { var tracks = new ArrayList(); for (var value : json.get("data").values()) { - tracks.add(this.parseTrack(value)); + tracks.add(this.parseTrack(value, preview, artistArtwork.get(parseArtistId(value)))); } return tracks; } - private AudioTrack parseTrack(JsonBrowser json) { + private List parseTracks(JsonBrowser json, boolean preview) throws IOException { + var ids = json.get("data").values().stream().map(this::parseArtistId).collect(Collectors.toList()); + return parseTracks(json, preview, getArtistCover(ids)); + } + + private AudioTrack parseTrack(JsonBrowser json, boolean preview, String artistArtwork) { var attributes = json.get("attributes"); return new AppleMusicAudioTrack( new AudioTrackInfo( @@ -285,6 +313,9 @@ private AudioTrack parseTrack(JsonBrowser json) { this.parseArtworkUrl(attributes.get("artwork")), attributes.get("isrc").text() ), + attributes.get("albumName").text(), + artistArtwork, + preview ? attributes.get("previews").index(0).get("hlsUrl").text() : null, this ); } @@ -293,23 +324,9 @@ 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); + private String parseArtistId(JsonBrowser json) { + var url = json.get("data").index(0).get("attributes").get("artistUrl").text(); + return url.substring(url.lastIndexOf('/')); } } diff --git a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java index cfaf7429..56813007 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java +++ b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioSourceManager.java @@ -1,7 +1,7 @@ package com.github.topi314.lavasrc.deezer; +import com.github.topi314.lavasrc.ExtendedAudioSourceManager; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; @@ -16,7 +16,6 @@ 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; @@ -26,11 +25,12 @@ import java.util.function.Function; import java.util.regex.Pattern; -public class DeezerAudioSourceManager implements AudioSourceManager, HttpConfigurable { +public class DeezerAudioSourceManager extends ExtendedAudioSourceManager implements 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 PREVIEW_PREFIX = "dzprev:"; 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"; @@ -39,7 +39,6 @@ public class DeezerAudioSourceManager implements AudioSourceManager, HttpConfigu private static final Logger log = LoggerFactory.getLogger(DeezerAudioSourceManager.class); private final String masterDecryptionKey; - private final HttpInterfaceManager httpInterfaceManager; public DeezerAudioSourceManager(String masterDecryptionKey) { @@ -55,33 +54,46 @@ public String getSourceName() { return "deezer"; } + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + var extendedAudioTrackInfo = super.decodeTrack(input); + return new DeezerAudioTrack(trackInfo, extendedAudioTrackInfo.albumName, extendedAudioTrackInfo.artistArtworkUrl, extendedAudioTrackInfo.previewUrl, this); + + } + @Override public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + var identifier = reference.identifier; + var preview = reference.identifier.startsWith(PREVIEW_PREFIX); + return this.loadItem(preview ? identifier.substring(PREVIEW_PREFIX.length()) : identifier, preview); + } + + public AudioItem loadItem(String identifier, boolean preview) { try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); + if (identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(identifier.substring(SEARCH_PREFIX.length()), preview); } - if (reference.identifier.startsWith(ISRC_PREFIX)) { - return this.getTrackByISRC(reference.identifier.substring(ISRC_PREFIX.length())); + if (identifier.startsWith(ISRC_PREFIX)) { + return this.getTrackByISRC(identifier.substring(ISRC_PREFIX.length()), preview); } // 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); + if (identifier.startsWith(SHARE_URL)) { + var request = new HttpGet(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 this.loadItem(location, preview); } } return null; } } - var matcher = URL_PATTERN.matcher(reference.identifier); + var matcher = URL_PATTERN.matcher(identifier); if (!matcher.find()) { return null; } @@ -89,16 +101,16 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) var id = matcher.group("identifier"); switch (matcher.group("type")) { case "album": - return this.getAlbum(id); + return this.getAlbum(id, preview); case "track": - return this.getTrack(id); + return this.getTrack(id, preview); case "playlist": - return this.getPlaylist(id); + return this.getPlaylist(id, preview); case "artist": - return this.getArtist(id); + return this.getArtist(id, preview); } } catch (IOException e) { @@ -113,7 +125,7 @@ public JsonBrowser getJson(String uri) throws IOException { return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); } - private List parseTracks(JsonBrowser json) { + private List parseTracks(JsonBrowser json, boolean preview) { var tracks = new ArrayList(); for (var track : json.get("data").values()) { if (!track.get("type").text().equals("track")) { @@ -123,12 +135,12 @@ private List parseTracks(JsonBrowser json) { log.warn("Skipping track {} by {} because it is not readable. Available countries: {}", track.get("title").text(), track.get("artist").get("name").text(), track.get("available_countries").text()); continue; } - tracks.add(this.parseTrack(track)); + tracks.add(this.parseTrack(track, preview)); } return tracks; } - private AudioTrack parseTrack(JsonBrowser json) { + private AudioTrack parseTrack(JsonBrowser json, boolean preview) { if (!json.get("readable").as(Boolean.class)) { throw new FriendlyException("This track is not readable. Available countries: " + json.get("available_countries").text(), FriendlyException.Severity.COMMON, null); } @@ -144,29 +156,31 @@ private AudioTrack parseTrack(JsonBrowser json) { json.get("album").get("cover_xl").text(), json.get("isrc").text() ), + json.get("album").get("title").text(), + json.get("artist").get("picture_xl").text(), + preview ? json.get("preview").text() : null, this ); } - private AudioItem getTrackByISRC(String isrc) throws IOException { + private AudioItem getTrackByISRC(String isrc, boolean preview) 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); + return this.parseTrack(json, preview); } - private AudioItem getSearch(String query) throws IOException { + private AudioItem getSearch(String query, boolean preview) 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); + return new BasicAudioPlaylist("Deezer Search: " + query, this.parseTracks(json, preview), null, true); } - private AudioItem getAlbum(String id) throws IOException { + private AudioItem getAlbum(String id, boolean preview) 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; @@ -174,18 +188,23 @@ private AudioItem getAlbum(String id) throws IOException { 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", json.get("link").text(), artworkUrl, author); + + for (var track : json.get("tracks").get("data").values()) { + track.get("artist").put("picture_xl", json.get("artist").get("picture_xl")); + } + + return new DeezerAudioPlaylist(json.get("title").text(), this.parseTracks(json.get("tracks"), preview), "album", json.get("link").text(), artworkUrl, author); } - private AudioItem getTrack(String id) throws IOException { + private AudioItem getTrack(String id, boolean preview) throws IOException { var json = this.getJson(PUBLIC_API_BASE + "/track/" + id); if (json == null) { return AudioReference.NO_TRACK; } - return this.parseTrack(json); + return this.parseTrack(json, preview); } - private AudioItem getPlaylist(String id) throws IOException { + private AudioItem getPlaylist(String id, boolean preview) 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; @@ -193,32 +212,26 @@ private AudioItem getPlaylist(String id) throws IOException { 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", json.get("link").text(), 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 tracks = this.getJson(PUBLIC_API_BASE + "/playlist/" + id + "/tracks"); - 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", json.get("link").text(), artworkUrl, author); + return new DeezerAudioPlaylist(json.get("title").text(), this.parseTracks(tracks, preview), "playlist", json.get("link").text(), artworkUrl, author); } - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } + private AudioItem getArtist(String id, boolean preview) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/artist/" + id); + if (json == null) { + return AudioReference.NO_TRACK; + } - @Override - public void encodeTrack(AudioTrack track, DataOutput output) { - } + var tracks = this.getJson(PUBLIC_API_BASE + "/artist/" + id + "/top?limit=50"); + if (json.get("data").values().isEmpty()) { + return AudioReference.NO_TRACK; + } - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { - return new DeezerAudioTrack(trackInfo, this); + var artworkUrl = json.get("picture_xl").text(); + var author = json.get("name").text(); + return new DeezerAudioPlaylist(author + "'s Top Tracks", this.parseTracks(tracks, preview), "artist", json.get("link").text(), artworkUrl, author); } @Override diff --git a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java index e854fc72..64946edc 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java +++ b/main/src/main/java/com/github/topi314/lavasrc/deezer/DeezerAudioTrack.java @@ -1,12 +1,12 @@ package com.github.topi314.lavasrc.deezer; +import com.github.topi314.lavasrc.ExtendedAudioTrack; import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3AudioTrack; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; import org.apache.commons.codec.binary.Hex; import org.apache.http.client.methods.HttpPost; @@ -20,12 +20,12 @@ import java.security.NoSuchAlgorithmException; import java.util.stream.Collectors; -public class DeezerAudioTrack extends DelegatedAudioTrack { +public class DeezerAudioTrack extends ExtendedAudioTrack { private final DeezerAudioSourceManager sourceManager; - public DeezerAudioTrack(AudioTrackInfo trackInfo, DeezerAudioSourceManager sourceManager) { - super(trackInfo); + public DeezerAudioTrack(AudioTrackInfo trackInfo, String albumName, String artistArtworkUrl, String previewUrl, DeezerAudioSourceManager sourceManager) { + super(trackInfo, albumName, artistArtworkUrl, previewUrl); this.sourceManager = sourceManager; } @@ -81,7 +81,7 @@ private byte[] getTrackDecryptionKey() throws NoSuchAlgorithmException { @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())) { + try (var stream = new DeezerPersistentHttpStream(httpInterface, this.previewUrl == null ? this.getTrackMediaURI() : new URI(this.previewUrl), this.trackInfo.length, this.getTrackDecryptionKey())) { processDelegate(new Mp3AudioTrack(this.trackInfo, stream), executor); } } @@ -89,7 +89,7 @@ public void process(LocalAudioTrackExecutor executor) throws Exception { @Override protected AudioTrack makeShallowClone() { - return new DeezerAudioTrack(this.trackInfo, this.sourceManager); + return new DeezerAudioTrack(this.trackInfo, this.albumName, this.artistArtworkUrl, this.previewUrl, this.sourceManager); } @Override diff --git a/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioSourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioSourceManager.java index b2de99d8..701056cd 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioSourceManager.java +++ b/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioSourceManager.java @@ -1,9 +1,24 @@ package com.github.topi314.lavasrc.mirror; +import com.github.topi314.lavasrc.ExtendedAudioSourceManager; +import com.github.topi314.lavasrc.applemusic.AppleMusicSourceManager; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.HttpClientBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public abstract class MirroringAudioSourceManager implements AudioSourceManager { +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class MirroringAudioSourceManager extends ExtendedAudioSourceManager implements HttpConfigurable { + + private static final Logger log = LoggerFactory.getLogger(MirroringAudioSourceManager.class); public static final String ISRC_PATTERN = "%ISRC%"; public static final String QUERY_PATTERN = "%QUERY%"; @@ -11,11 +26,36 @@ public abstract class MirroringAudioSourceManager implements AudioSourceManager protected final AudioPlayerManager audioPlayerManager; protected final MirroringAudioTrackResolver resolver; + protected final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + protected MirroringAudioSourceManager(AudioPlayerManager audioPlayerManager, MirroringAudioTrackResolver resolver) { this.audioPlayerManager = audioPlayerManager; this.resolver = resolver; } + @Override + public void configureRequests(Function configurator) { + this.httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + this.httpInterfaceManager.configureBuilder(configurator); + } + + public HttpInterface getHttpInterface() { + return this.httpInterfaceManager.getInterface(); + } + + @Override + public void shutdown() { + try { + this.httpInterfaceManager.close(); + } catch (IOException e) { + log.error("Failed to close HTTP interface manager", e); + } + } + public AudioPlayerManager getAudioPlayerManager() { return this.audioPlayerManager; } diff --git a/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioTrack.java b/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioTrack.java index e6181d41..f16a7071 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioTrack.java +++ b/main/src/main/java/com/github/topi314/lavasrc/mirror/MirroringAudioTrack.java @@ -1,28 +1,44 @@ package com.github.topi314.lavasrc.mirror; +import com.github.topi314.lavasrc.ExtendedAudioTrack; +import com.github.topi314.lavasrc.deezer.DeezerPersistentHttpStream; +import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3AudioTrack; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream; +import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.*; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URI; import java.util.concurrent.CompletableFuture; -public abstract class MirroringAudioTrack extends DelegatedAudioTrack { +public abstract class MirroringAudioTrack extends ExtendedAudioTrack { private static final Logger log = LoggerFactory.getLogger(MirroringAudioTrack.class); protected final MirroringAudioSourceManager sourceManager; - public MirroringAudioTrack(AudioTrackInfo trackInfo, MirroringAudioSourceManager sourceManager) { - super(trackInfo); + public MirroringAudioTrack(AudioTrackInfo trackInfo, String albumName, String artistArtworkUrl, String previewUrl, MirroringAudioSourceManager sourceManager) { + super(trackInfo, albumName, artistArtworkUrl, previewUrl); this.sourceManager = sourceManager; } + abstract protected InternalAudioTrack createAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream); + @Override public void process(LocalAudioTrackExecutor executor) throws Exception { + if (this.previewUrl != null) { + try (var httpInterface = this.sourceManager.getHttpInterface()) { + try (var stream = new PersistentHttpStream(httpInterface, new URI(this.previewUrl), this.trackInfo.length)) { + processDelegate(createAudioTrack(this.trackInfo, stream), executor); + } + } + return; + } AudioItem track = this.sourceManager.getResolver().apply(this); if (track instanceof AudioPlaylist) { diff --git a/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifyAudioTrack.java b/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifyAudioTrack.java index 19063ff8..d84a285f 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifyAudioTrack.java +++ b/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifyAudioTrack.java @@ -1,13 +1,28 @@ package com.github.topi314.lavasrc.spotify; import com.github.topi314.lavasrc.mirror.MirroringAudioTrack; +import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3AudioTrack; +import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegAudioTrack; +import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; +import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; public class SpotifyAudioTrack extends MirroringAudioTrack { + public SpotifyAudioTrack(AudioTrackInfo trackInfo, SpotifySourceManager sourceManager) { - super(trackInfo, sourceManager); + this(trackInfo, null, null, null, sourceManager); + } + + public SpotifyAudioTrack(AudioTrackInfo trackInfo, String albumName, String artistArtworkUrl, String previewUrl, SpotifySourceManager sourceManager) { + super(trackInfo, albumName, artistArtworkUrl, previewUrl, sourceManager); + } + + @Override + protected InternalAudioTrack createAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream stream) { + return new Mp3AudioTrack(trackInfo, stream); } @Override diff --git a/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifySourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifySourceManager.java index 0161658e..bcebb1d6 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifySourceManager.java +++ b/main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifySourceManager.java @@ -19,7 +19,6 @@ 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; @@ -37,6 +36,7 @@ public class SpotifySourceManager extends MirroringAudioSourceManager implements public static final Pattern URL_PATTERN = Pattern.compile("(https?://)(www\\.)?open\\.spotify\\.com/((?[a-zA-Z-]+)/)?(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 String PREVIEW_PREFIX = "spprev:"; 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/"; @@ -88,31 +88,29 @@ public String getSourceName() { } @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { - return new SpotifyAudioTrack(trackInfo, this); + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + var extendedAudioTrackInfo = super.decodeTrack(input); + return new SpotifyAudioTrack(trackInfo, extendedAudioTrackInfo.albumName, extendedAudioTrackInfo.artistArtworkUrl, extendedAudioTrackInfo.previewUrl, this); } @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) { + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + var identifier = reference.identifier; + var preview = reference.identifier.startsWith(PREVIEW_PREFIX); + return this.loadItem(preview ? identifier.substring(PREVIEW_PREFIX.length()) : identifier, preview); } - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + public AudioItem loadItem(String identifier, boolean preview) { try { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length()).trim()); + if (identifier.startsWith(SEARCH_PREFIX)) { + return this.getSearch(identifier.substring(SEARCH_PREFIX.length()).trim(), preview); } - if (reference.identifier.startsWith(RECOMMENDATIONS_PREFIX)) { - return this.getRecommendations(reference.identifier.substring(RECOMMENDATIONS_PREFIX.length()).trim()); + if (identifier.startsWith(RECOMMENDATIONS_PREFIX)) { + return this.getRecommendations(identifier.substring(RECOMMENDATIONS_PREFIX.length()).trim(), preview); } - var matcher = URL_PATTERN.matcher(reference.identifier); + var matcher = URL_PATTERN.matcher(identifier); if (!matcher.find()) { return null; } @@ -120,16 +118,16 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) var id = matcher.group("identifier"); switch (matcher.group("type")) { case "album": - return this.getAlbum(id); + return this.getAlbum(id, preview); case "track": - return this.getTrack(id); + return this.getTrack(id, preview); case "playlist": - return this.getPlaylist(id); + return this.getPlaylist(id, preview); case "artist": - return this.getArtist(id); + return this.getArtist(id, preview); } } catch (IOException e) { throw new RuntimeException(e); @@ -160,25 +158,25 @@ public JsonBrowser getJson(String uri) throws IOException { return HttpClientTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request); } - public AudioItem getSearch(String query) throws IOException { + public AudioItem getSearch(String query, boolean preview) 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); + return new BasicAudioPlaylist("Search results for: " + query, this.parseTrackItems(json.get("tracks"), preview), null, true); } - public AudioItem getRecommendations(String query) throws IOException { + public AudioItem getRecommendations(String query, boolean preview) throws IOException { var json = this.getJson(API_BASE + "recommendations?" + query); if (json == null || json.get("tracks").values().isEmpty()) { return AudioReference.NO_TRACK; } - return new SpotifyAudioPlaylist("Spotify Recommendations:", this.parseTracks(json), "recommendations", null, null, null); + return new SpotifyAudioPlaylist("Spotify Recommendations:", this.parseTracks(json, preview), "recommendations", null, null, null); } - public AudioItem getAlbum(String id) throws IOException { + public AudioItem getAlbum(String id, boolean preview) throws IOException { var json = this.getJson(API_BASE + "albums/" + id); if (json == null) { return AudioReference.NO_TRACK; @@ -194,7 +192,15 @@ public AudioItem getAlbum(String id) throws IOException { var tracksPage = this.getJson(API_BASE + "tracks/?ids=" + page.get("items").values().stream().map(track -> track.get("id").text()).collect(Collectors.joining(","))); - tracks.addAll(this.parseTracks(tracksPage)); + for (var track : tracksPage.get("tracks").values()) { + var albumJson = JsonBrowser.newMap(); + albumJson.put("name", json.get("name")); + track.put("album", albumJson); + + var artistsJson = JsonBrowser.newList(); + artistsJson.add(json.get("artists").index(0)); + } + tracks.addAll(this.parseTracks(tracksPage, preview)); } while (page.get("next").text() != null && ++pages < this.albumPageLimit); @@ -206,7 +212,7 @@ public AudioItem getAlbum(String id) throws IOException { } - public AudioItem getPlaylist(String id) throws IOException { + public AudioItem getPlaylist(String id, boolean preview) throws IOException { var json = this.getJson(API_BASE + "playlists/" + id); if (json == null) { return AudioReference.NO_TRACK; @@ -225,7 +231,7 @@ public AudioItem getPlaylist(String id) throws IOException { if (track.isNull() || track.get("is_local").asBoolean(false)) { continue; } - tracks.add(this.parseTrack(track)); + tracks.add(this.parseTrack(track, preview)); } } @@ -239,42 +245,42 @@ public AudioItem getPlaylist(String id) throws IOException { } - public AudioItem getArtist(String id) throws IOException { + public AudioItem getArtist(String id, boolean preview) 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 SpotifyAudioPlaylist(json.get("tracks").index(0).get("artists").index(0).get("name").text() + "'s Top Tracks", this.parseTracks(json), "artist", json.get("tracks").index(0).get("external_urls").get("spotify").text(), json.get("tracks").index(0).get("album").get("images").index(0).get("url").text(), json.get("tracks").index(0).get("artists").index(0).get("name").text()); + return new SpotifyAudioPlaylist(json.get("tracks").index(0).get("artists").index(0).get("name").text() + "'s Top Tracks", this.parseTracks(json, preview), "artist", json.get("tracks").index(0).get("external_urls").get("spotify").text(), json.get("tracks").index(0).get("album").get("images").index(0).get("url").text(), json.get("tracks").index(0).get("artists").index(0).get("name").text()); } - public AudioItem getTrack(String id) throws IOException { + public AudioItem getTrack(String id, boolean preview) throws IOException { var json = this.getJson(API_BASE + "tracks/" + id); if (json == null) { return AudioReference.NO_TRACK; } - return this.parseTrack(json); + return this.parseTrack(json, preview); } - private List parseTracks(JsonBrowser json) { + private List parseTracks(JsonBrowser json, boolean preview) { var tracks = new ArrayList(); for (var value : json.get("tracks").values()) { - tracks.add(this.parseTrack(value)); + tracks.add(this.parseTrack(value, preview)); } return tracks; } - private List parseTrackItems(JsonBrowser json) { + private List parseTrackItems(JsonBrowser json, boolean preview) { var tracks = new ArrayList(); for (var value : json.get("items").values()) { if (value.get("is_local").asBoolean(false)) { continue; } - tracks.add(this.parseTrack(value)); + tracks.add(this.parseTrack(value, preview)); } return tracks; } - private AudioTrack parseTrack(JsonBrowser json) { + private AudioTrack parseTrack(JsonBrowser json, boolean preview) { return new SpotifyAudioTrack( new AudioTrackInfo( json.get("name").text(), @@ -286,6 +292,9 @@ private AudioTrack parseTrack(JsonBrowser json) { json.get("album").get("images").index(0).get("url").text(), json.get("external_ids").get("isrc").text() ), + json.get("album").get("name").text(), + json.get("artists").index(0).get("images").index(0).get("url").text(), + preview ? json.get("preview_url").text() : null, this ); } diff --git a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java index a4b5dc30..dbc41082 100644 --- a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java +++ b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java @@ -1,11 +1,14 @@ package com.github.topi314.lavasrc.plugin; import com.github.topi314.lavasrc.ExtendedAudioPlaylist; +import com.github.topi314.lavasrc.ExtendedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import dev.arbjerg.lavalink.api.AudioPluginInfoModifier; import kotlinx.serialization.json.JsonElementKt; import kotlinx.serialization.json.JsonObject; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.springframework.stereotype.Component; import java.util.Map; @@ -25,4 +28,17 @@ public JsonObject modifyAudioPlaylistPluginInfo(@NotNull AudioPlaylist playlist) } return null; } + + @Nullable + @Override + public JsonObject modifyAudioTrackPluginInfo(@NotNull AudioTrack track) { + if (track instanceof ExtendedAudioTrack extendedTrack) { + return new JsonObject(Map.of( + "albumName", JsonElementKt.JsonPrimitive(extendedTrack.getAlbumName()), + "artistArtworkUrl", JsonElementKt.JsonPrimitive(extendedTrack.getArtistArtworkUrl()), + "previewUrl", JsonElementKt.JsonPrimitive(extendedTrack.getPreviewUrl()) + )); + } + return null; + } }