diff --git a/main/build.gradle b/main/build.gradle index e97b0f15..63077f5d 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -5,9 +5,11 @@ plugins { archivesBaseName = "lavasrc" dependencies { - compileOnly "com.github.walkyst:lavaplayer-fork:1.3.98.4" + 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" + compileOnly "org.slf4j:slf4j-api:1.7.25" } publishing { 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 new file mode 100644 index 00000000..462461fd --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/applemusic/AppleMusicAudioPlaylist.java @@ -0,0 +1,14 @@ +package com.github.topisenpai.lavasrc.applemusic; + +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import java.util.List; + +public class AppleMusicAudioPlaylist extends ExtendedAudioPlaylist { + + public AppleMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, type, identifier, artworkURL, 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 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 d20e6f7a..17d7f123 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 @@ -4,7 +4,6 @@ import com.github.topisenpai.lavasrc.mirror.MirroringAudioSourceManager; import com.github.topisenpai.lavasrc.mirror.MirroringAudioTrackResolver; 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; @@ -23,6 +22,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; @@ -83,12 +83,17 @@ 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 @@ -210,7 +215,9 @@ public AudioItem getAlbum(String id, String countryCode) throws IOException { return AudioReference.NO_TRACK; } - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, null, false); + 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 { @@ -235,7 +242,9 @@ public AudioItem getPlaylist(String id, String countryCode) throws IOException { return AudioReference.NO_TRACK; } - return new BasicAudioPlaylist(json.get("data").index(0).get("attributes").get("name").text(), tracks, null, false); + 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 { @@ -243,7 +252,12 @@ public AudioItem getArtist(String id, String countryCode) throws IOException { 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); + + 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 { @@ -264,7 +278,6 @@ private List parseTracks(JsonBrowser json) { private AudioTrack parseTrack(JsonBrowser json) { var attributes = json.get("attributes"); - var artwork = attributes.get("artwork"); return new AppleMusicAudioTrack( new AudioTrackInfo( attributes.get("name").text(), @@ -272,14 +285,18 @@ 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(), - artwork.get("url").text().replace("{w}", artwork.get("width").text()).replace("{h}", artwork.get("height").text()), 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 { 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..3f9f80c7 --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerAudioPlaylist.java @@ -0,0 +1,14 @@ +package com.github.topisenpai.lavasrc.deezer; + +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import java.util.List; + +public class DeezerAudioPlaylist extends ExtendedAudioPlaylist { + + public DeezerAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, type, identifier, artworkURL, 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 976890af..d3331273 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 ); } @@ -166,7 +167,10 @@ private AudioItem getAlbum(String id) throws IOException { 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); + + 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 { @@ -182,7 +186,10 @@ private AudioItem getPlaylist(String id) throws IOException { 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); + + 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 { @@ -190,7 +197,10 @@ private AudioItem getArtist(String id) throws IOException { 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); + + 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 @@ -199,19 +209,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()); + public void encodeTrack(AudioTrack track, DataOutput output) { } @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new DeezerAudioTrack(trackInfo, - DataFormatTools.readNullableText(input), - DataFormatTools.readNullableText(input), - this - ); + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + 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 70a52d3a..4d861db7 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 @@ -22,25 +22,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); @@ -101,7 +89,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/deezer/DeezerPersistentHttpStream.java b/main/src/main/java/com/github/topisenpai/lavasrc/deezer/DeezerPersistentHttpStream.java index 5f4a40ce..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; 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 ef7b2d6e..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/DefaultMirroringAudioTrackResolver.java b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/DefaultMirroringAudioTrackResolver.java index 745a29fb..abe8faf5 100644 --- a/main/src/main/java/com/github/topisenpai/lavasrc/mirror/DefaultMirroringAudioTrackResolver.java +++ b/main/src/main/java/com/github/topisenpai/lavasrc/mirror/DefaultMirroringAudioTrackResolver.java @@ -40,8 +40,8 @@ public AudioItem apply(MirroringAudioTrack mirroringAudioTrack) { } if (provider.contains(ISRC_PATTERN)) { - if (mirroringAudioTrack.getISRC() != null) { - provider = provider.replace(ISRC_PATTERN, mirroringAudioTrack.getISRC()); + if (mirroringAudioTrack.getInfo().isrc != null && !mirroringAudioTrack.getInfo().isrc.isEmpty()) { + provider = provider.replace(ISRC_PATTERN, mirroringAudioTrack.getInfo().isrc); } else { log.debug("Ignoring identifier \"{}\" because this track does not have an ISRC!", provider); continue; 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 18b8fd68..16889b22 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 @@ -2,11 +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.track.AudioTrack; - -import java.io.DataOutput; -import java.io.IOException; public abstract class MirroringAudioSourceManager implements AudioSourceManager { @@ -29,16 +24,4 @@ public MirroringAudioTrackResolver getResolver() { return this.resolver; } - @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 f010f11d..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; @@ -20,23 +21,17 @@ 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; + private AudioTrack delegate; - 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; + @Nullable + public AudioTrack getDelegate() { + return this.delegate; } @Override @@ -47,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/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..65816e3b --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/spotify/SpotifyAudioPlaylist.java @@ -0,0 +1,14 @@ +package com.github.topisenpai.lavasrc.spotify; + +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import java.util.List; + +public class SpotifyAudioPlaylist extends ExtendedAudioPlaylist { + + public SpotifyAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, type, identifier, artworkURL, 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 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 791b256b..b0c0409f 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 @@ -4,7 +4,6 @@ import com.github.topisenpai.lavasrc.mirror.MirroringAudioSourceManager; import com.github.topisenpai.lavasrc.mirror.MirroringAudioTrackResolver; 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; @@ -24,6 +23,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; @@ -92,12 +92,17 @@ 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 AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new SpotifyAudioTrack(trackInfo, this); + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) { } @Override @@ -281,10 +286,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/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..54f1246c --- /dev/null +++ b/main/src/main/java/com/github/topisenpai/lavasrc/yandexmusic/YandexMusicAudioPlaylist.java @@ -0,0 +1,14 @@ +package com.github.topisenpai.lavasrc.yandexmusic; + +import com.github.topisenpai.lavasrc.ExtendedAudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import java.util.List; + +public class YandexMusicAudioPlaylist extends ExtendedAudioPlaylist { + + public YandexMusicAudioPlaylist(String name, List tracks, String type, String identifier, String artworkURL, String author) { + super(name, tracks, type, identifier, artworkURL, 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 a7ed0f6b..391ec2b1 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,19 +17,14 @@ import java.security.NoSuchAlgorithmException; public class YandexMusicAudioTrack extends DelegatedAudioTrack { - private final String artworkURL; + private final YandexMusicSourceManager sourceManager; - public YandexMusicAudioTrack(AudioTrackInfo trackInfo, String artworkURL, YandexMusicSourceManager sourceManager) { + public YandexMusicAudioTrack(AudioTrackInfo trackInfo, YandexMusicSourceManager sourceManager) { super(trackInfo); - this.artworkURL = artworkURL; this.sourceManager = sourceManager; } - public String getArtworkURL() { - return this.artworkURL; - } - @Override public void process(LocalAudioTrackExecutor executor) throws Exception { var downloadLink = this.getDownloadURL(this.trackInfo.identifier); @@ -42,7 +37,7 @@ public void process(LocalAudioTrackExecutor executor) throws Exception { @Override protected AudioTrack makeShallowClone() { - return new YandexMusicAudioTrack(this.trackInfo, this.artworkURL, this.sourceManager); + return new YandexMusicAudioTrack(this.trackInfo, this.sourceManager); } @Override 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 eb50f9c4..ccf9601c 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 @@ -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; @@ -116,7 +115,9 @@ private AudioItem getAlbum(String id) throws IOException { if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } - return new BasicAudioPlaylist(json.get("result").get("title").text(), tracks, null, false); + 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 { @@ -132,12 +133,16 @@ private AudioItem getArtist(String id) throws IOException { 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); + + 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 { @@ -152,8 +157,10 @@ private AudioItem getPlaylist(String userString, String id) throws IOException { 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); + 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 { @@ -188,32 +195,37 @@ private AudioTrack parseTrack(JsonBrowser json) { 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, + 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), + null + ), 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()); + public void encodeTrack(AudioTrack track, DataOutput output) { } @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new YandexMusicAudioTrack(trackInfo, DataFormatTools.readNullableText(input), this); + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new YandexMusicAudioTrack(trackInfo, this); } @Override diff --git a/plugin/build.gradle b/plugin/build.gradle index ca4875c9..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("dev.arbjerg.lavalink:plugin-api:3.6.1") - // runtimeOnly("com.github.freyacodes.lavalink:Lavalink-Server:3.6.0") + 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 new file mode 100644 index 00000000..2d26440c --- /dev/null +++ b/plugin/src/main/java/com/github/topisenpai/lavasrc/plugin/LavaSrcAudioPluginInfoModifier.java @@ -0,0 +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()) + )); + } + return null; + } + +}