diff --git a/README.md b/README.md index cf7d712c..98cf1e35 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,9 @@ plugins: masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else) yandexmusic: accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music + playlistLoadLimit: 1 # The number of pages at 100 tracks each + albumLoadLimit: 1 # The number of pages at 50 tracks each + artistLoadLimit: 1 # The number of pages at 10 tracks each flowerytts: voice: "default voice" # (case-sensitive) get default voice from here https://api.flowery.pw/v1/tts/voices translate: false # whether to translate the text to the native language of voice @@ -334,6 +337,7 @@ LavaSrc adds the following fields to tracks & playlists in Lavalink ### Yandex Music * `ymsearch:animals architects` +* `ymrec:71663565` (`ymrec:identifier`, you can get the identifier from Lavalink's "info.identifier" response to Yandex music loadtracks) * https://music.yandex.ru/album/13886032/track/71663565 * https://music.yandex.ru/album/13886032 * https://music.yandex.ru/track/71663565 diff --git a/application.example.yml b/application.example.yml index 867388d5..4b198668 100644 --- a/application.example.yml +++ b/application.example.yml @@ -33,6 +33,9 @@ plugins: masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else) yandexmusic: accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music + playlistLoadLimit: 1 # The number of pages at 100 tracks each + albumLoadLimit: 1 # The number of pages at 50 tracks each + artistLoadLimit: 1 # The number of pages at 10 tracks each flowerytts: voice: "default voice" # (case-sensitive) get default voice here https://flowery.pw/docs/flowery/tts-voices-v-1-tts-voices-get translate: false # whether to translate the text to the native language of voice diff --git a/main/src/main/java/com/github/topi314/lavasrc/yandexmusic/YandexMusicSourceManager.java b/main/src/main/java/com/github/topi314/lavasrc/yandexmusic/YandexMusicSourceManager.java index e0de0371..b80c8cf4 100644 --- a/main/src/main/java/com/github/topi314/lavasrc/yandexmusic/YandexMusicSourceManager.java +++ b/main/src/main/java/com/github/topi314/lavasrc/yandexmusic/YandexMusicSourceManager.java @@ -28,16 +28,23 @@ import java.util.stream.Collectors; public class YandexMusicSourceManager extends ExtendedAudioSourceManager implements HttpConfigurable { - public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.(ru|com)/(?artist|album|track)/(?[0-9]+)(/(?track)/(?[0-9]+))?/?"); - public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.(ru|com)/users/(?[0-9A-Za-z@.-]+)/playlists/(?[0-9]+)/?"); + public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.(?ru|com|kz|by)/(?artist|album|track)/(?[0-9]+)(/(?track)/(?[0-9]+))?/?"); + public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile("(https?://)?music\\.yandex\\.(?ru|com|kz|by)/users/(?[0-9A-Za-z@.-]+)/playlists/(?[0-9]+)/?"); public static final String SEARCH_PREFIX = "ymsearch:"; + public static final String RECOMMENDATIONS_PREFIX = "ymrec:"; public static final String PUBLIC_API_BASE = "https://api.music.yandex.net"; + public static final int ARTIST_MAX_PAGE_ITEMS = 10; + public static final int PLAYLIST_MAX_PAGE_ITEMS = 100; + public static final int ALBUM_MAX_PAGE_ITEMS = 50; private static final Logger log = LoggerFactory.getLogger(YandexMusicSourceManager.class); private final HttpInterfaceManager httpInterfaceManager; private final String accessToken; + private int artistLoadLimit; + private int albumLoadLimit; + private int playlistLoadLimit; public YandexMusicSourceManager(String accessToken) { if (accessToken == null || accessToken.isEmpty()) { @@ -47,6 +54,16 @@ public YandexMusicSourceManager(String accessToken) { this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); } + public void setArtistLoadLimit(int artistLimit) { + this.artistLoadLimit = artistLimit; + } + public void setAlbumLoadLimit(int albumLimit) { + this.albumLoadLimit = albumLimit; + } + public void setPlaylistLoadLimit(int playlistLimit) { + this.playlistLoadLimit = playlistLimit; + } + @Override public String getSourceName() { return "yandexmusic"; @@ -59,22 +76,27 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) return this.getSearch(reference.identifier.substring(SEARCH_PREFIX.length())); } + if (reference.identifier.startsWith(RECOMMENDATIONS_PREFIX)) { + return this.getRecommendations(reference.identifier.substring(RECOMMENDATIONS_PREFIX.length())); + } + var matcher = URL_PATTERN.matcher(reference.identifier); if (matcher.find()) { + var domainEnd = matcher.group("domain"); switch (matcher.group("type1")) { case "album": if (matcher.group("type2") != null) { var trackId = matcher.group("identifier2"); - return this.getTrack(trackId); + return this.getTrack(trackId, domainEnd); } var albumId = matcher.group("identifier"); - return this.getAlbum(albumId); + return this.getAlbum(albumId, domainEnd); case "artist": var artistId = matcher.group("identifier"); - return this.getArtist(artistId); + return this.getArtist(artistId, domainEnd); case "track": var trackId = matcher.group("identifier"); - return this.getTrack(trackId); + return this.getTrack(trackId, domainEnd); } return null; } @@ -82,7 +104,7 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) if (matcher.find()) { var userId = matcher.group("identifier"); var playlistId = matcher.group("identifier2"); - return this.getPlaylist(userId, playlistId); + return this.getPlaylist(userId, playlistId, matcher.group("domain")); } } catch (IOException e) { throw new RuntimeException(e); @@ -90,27 +112,61 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) return null; } + private static boolean canBeLong(String str) { + try { + Long.parseLong(str); + return true; + } catch(NumberFormatException e) { + return false; + } + } + + private AudioItem getRecommendations(String identifier) throws IOException { + if (!canBeLong(identifier)) { + throw new IllegalArgumentException("The yandex music track identifier must be a number"); + } + + var json = this.getJson(PUBLIC_API_BASE + "/tracks/"+identifier+"/similar"); + if (json.isNull() || json.get("result").isNull() || json.get("result").get("similarTracks").values().isEmpty()) { + return AudioReference.NO_TRACK; + } + var tracks = this.parseTracks(json.get("result").get("similarTracks"), "com"); + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + var trackInfo = json.get("result").get("track"); + return new YandexMusicAudioPlaylist( + "Yandex Music Recommendations For Track: " + trackInfo.get("title").text(), + tracks, + ExtendedAudioPlaylist.Type.RECOMMENDATIONS, + null, + null, + null, + tracks.size() + ); + } + private AudioItem getSearch(String query) throws IOException { var json = this.getJson(PUBLIC_API_BASE + "/search?text=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track&page=0"); if (json.isNull() || json.get("result").get("tracks").isNull()) { return AudioReference.NO_TRACK; } - var tracks = this.parseTracks(json.get("result").get("tracks").get("results")); + var tracks = this.parseTracks(json.get("result").get("tracks").get("results"), "com"); if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } return new BasicAudioPlaylist("Yandex Music Search: " + query, tracks, null, true); } - private AudioItem getAlbum(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/albums/" + id + "/with-tracks"); + private AudioItem getAlbum(String id, String domainEnd) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/albums/" + id + "/with-tracks?page-size=" + ALBUM_MAX_PAGE_ITEMS * albumLoadLimit); if (json.isNull() || json.get("result").isNull()) { return AudioReference.NO_TRACK; } var tracks = new ArrayList(); for (var volume : json.get("result").get("volumes").values()) { for (var track : volume.values()) { - var parsedTrack = this.parseTrack(track); + var parsedTrack = this.parseTrack(track, domainEnd); if (parsedTrack != null) { tracks.add(parsedTrack); } @@ -119,34 +175,33 @@ private AudioItem getAlbum(String id) throws IOException { if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } - var coverUri = json.get("result").get("coverUri").text(); var author = json.get("result").get("artists").values().get(0).get("name").text(); return new YandexMusicAudioPlaylist( json.get("result").get("title").text(), tracks, ExtendedAudioPlaylist.Type.ALBUM, - "https://music.yandex.ru/album/" + id, - this.formatCoverUri(coverUri), + "https://music.yandex." + domainEnd + "/album/" + id, + this.parseCoverUri(json.get("result")), author, tracks.size() ); } - private AudioItem getTrack(String id) throws IOException { + private AudioItem getTrack(String id, String domainEnd) throws IOException { var json = this.getJson(PUBLIC_API_BASE + "/tracks/" + id); if (json.isNull() || json.get("result").values().get(0).get("available").text().equals("false")) { return AudioReference.NO_TRACK; } - return this.parseTrack(json.get("result").values().get(0)); + return this.parseTrack(json.get("result").values().get(0), domainEnd); } - private AudioItem getArtist(String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/artists/" + id + "/tracks?page-size=10"); + private AudioItem getArtist(String id, String domainEnd) throws IOException { + var json = this.getJson(PUBLIC_API_BASE + "/artists/" + id + "/tracks?page-size=" + ARTIST_MAX_PAGE_ITEMS * artistLoadLimit); if (json.isNull() || json.get("result").values().isEmpty()) { return AudioReference.NO_TRACK; } - var tracks = this.parseTracks(json.get("result").get("tracks")); + var tracks = this.parseTracks(json.get("result").get("tracks"), domainEnd); if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } @@ -155,41 +210,27 @@ private AudioItem getArtist(String id) throws IOException { var artistJson = artistJsonResponse.get("result").get("artist"); var author = artistJson.get("name").text(); - String coverUri = null; - - if (!artistJson.get("ogImage").isNull()) { - coverUri = this.formatCoverUri(artistJson.get("ogImage").text()); - } else if (!artistJson.get("cover").isNull()) { - coverUri = this.formatCoverUri(artistJson.get("cover").get("uri").text()); - } - return new YandexMusicAudioPlaylist( author + "'s Top Tracks", tracks, ExtendedAudioPlaylist.Type.ARTIST, "https://music.yandex.ru/artist/" + id, - coverUri, + parseCoverUri(artistJson), author, tracks.size() ); } - private AudioItem getPlaylist(String userString, String id) throws IOException { - var json = this.getJson(PUBLIC_API_BASE + "/users/" + userString + "/playlists/" + id); + private AudioItem getPlaylist(String userString, String id, String domainEnd) throws IOException { + var json = this.getJson( + PUBLIC_API_BASE + "/users/" + userString + "/playlists/" + id + + "?page-size=" + PLAYLIST_MAX_PAGE_ITEMS * playlistLoadLimit + + "&rich-tracks=true" + ); if (json.isNull() || json.get("result").isNull() || json.get("result").get("tracks").values().isEmpty()) { return AudioReference.NO_TRACK; } - var tracks = new ArrayList(); - var tracksToParse = json.get("result").get("tracks").values(); - if (tracksToParse.get(0).get("track").isNull()) { - tracksToParse = getTracks(getTrackIds(tracksToParse)); - } - for (var track : tracksToParse) { - var parsedTrack = track.get("track").isNull() ? this.parseTrack(track) : this.parseTrack(track.get("track")); - if (parsedTrack != null) { - tracks.add(parsedTrack); - } - } + var tracks = this.parseTracks(json.get("result").get("tracks"), domainEnd); if (tracks.isEmpty()) { return AudioReference.NO_TRACK; } @@ -201,29 +242,18 @@ private AudioItem getPlaylist(String userString, String id) throws IOException { } else { playlistTitle = 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, ExtendedAudioPlaylist.Type.PLAYLIST, - "https://music.yandex.ru/users/" + userString + "/playlists/" + id, - this.formatCoverUri(coverUri), + "https://music.yandex." + domainEnd + "/users/" + userString + "/playlists/" + id, + this.parseCoverUri(json.get("result")), author, tracks.size() ); } - private List getTracks(String trackIds) throws IOException { - return getJson(PUBLIC_API_BASE + "/tracks?track-ids=" + URLEncoder.encode(trackIds, StandardCharsets.UTF_8)).get("result").values(); - } - - private String getTrackIds(List tracksToParse) { - return tracksToParse.stream() - .map(node -> node.get("id").text()) - .collect(Collectors.joining(",")); - } - public JsonBrowser getJson(String uri) throws IOException { var request = new HttpGet(uri); request.setHeader("Accept", "application/json"); @@ -238,10 +268,11 @@ public String getDownloadStrings(String uri) throws IOException { return HttpClientTools.fetchResponseLines(this.httpInterfaceManager.getInterface(), request, "downloadinfo-xml-page")[0]; } - private List parseTracks(JsonBrowser json) { + private List parseTracks(JsonBrowser json, String domainEnd) { + var tracksToParse = json.values(); var tracks = new ArrayList(); - for (var track : json.values()) { - var parsedTrack = this.parseTrack(track); + for (var track : tracksToParse) { + var parsedTrack = track.get("track").isNull() ? this.parseTrack(track, domainEnd) : this.parseTrack(track.get("track"), domainEnd); if (parsedTrack != null) { tracks.add(parsedTrack); } @@ -249,30 +280,27 @@ private List parseTracks(JsonBrowser json) { return tracks; } - private AudioTrack parseTrack(JsonBrowser json) { + private AudioTrack parseTrack(JsonBrowser json, String domainEnd) { if (!json.get("available").asBoolean(false)) { return null; } var id = json.get("id").text(); var artist = parseArtist(json); - var coverUri = json.get("coverUri").text(); String albumName = null; String albumUrl = null; if (!json.get("albums").values().isEmpty()) { var album = json.get("albums").values().get(0); albumName = album.get("title").text(); - albumUrl = "https://music.yandex.ru/album/" + album.get("id").text(); + albumUrl = "https://music.yandex." + domainEnd + "/album/" + album.get("id").text(); } String artistUrl = null; String artistArtworkUrl = null; if (!json.get("artists").values().isEmpty()) { var firstArtist = json.get("artists").values().get(0); - artistUrl = "https://music.yandex.ru/artist/" + firstArtist.get("id").text(); - if (!firstArtist.get("cover").isNull()) { - artistArtworkUrl = this.formatCoverUri(firstArtist.get("cover").get("uri").text()); - } + artistUrl = "https://music.yandex." + domainEnd + "/artist/" + firstArtist.get("id").text(); + artistArtworkUrl = parseCoverUri(firstArtist); } return new YandexMusicAudioTrack( new AudioTrackInfo( @@ -281,8 +309,8 @@ private AudioTrack parseTrack(JsonBrowser json) { json.get("durationMs").as(Long.class), id, false, - "https://music.yandex.ru/track/" + id, - this.formatCoverUri(coverUri), + "https://music.yandex." + domainEnd + "/track/" + id, + this.parseCoverUri(json), null ), albumName, @@ -315,6 +343,26 @@ private String extractArtists(JsonBrowser artistNode) { .collect(Collectors.joining(", ")); } + private String parseCoverUri(JsonBrowser objectJson) { + if (!objectJson.get("ogImage").isNull()) { + return formatCoverUri(objectJson.get("ogImage").text()); + } + if (!objectJson.get("coverUri").isNull()) { + return formatCoverUri(objectJson.get("coverUri").text()); + } + + var coverJson = objectJson.get("cover"); + if (!coverJson.isNull()) { + if (!coverJson.get("uri").isNull()) { + return formatCoverUri(coverJson.get("uri").text()); + } else if (!coverJson.get("itemsUri").values().isEmpty()) { + return formatCoverUri(coverJson.get("itemsUri").values().get(0).text()); + } + } + + return null; + } + private String formatCoverUri(String coverUri) { return coverUri != null ? "https://" + coverUri.replace("%%", "400x400") : null; } diff --git a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java index ef206bcb..8f2a939d 100644 --- a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java +++ b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java @@ -61,6 +61,15 @@ public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, Ly } if (sourcesConfig.isYandexMusic()) { this.yandexMusic = new YandexMusicSourceManager(yandexMusicConfig.getAccessToken()); + if (yandexMusicConfig.getPlaylistLoadLimit() > 0) { + yandexMusic.setPlaylistLoadLimit(yandexMusicConfig.getPlaylistLoadLimit()); + } + if (yandexMusicConfig.getAlbumLoadLimit() > 0) { + yandexMusic.setAlbumLoadLimit(yandexMusicConfig.getAlbumLoadLimit()); + } + if (yandexMusicConfig.getArtistLoadLimit() > 0) { + yandexMusic.setArtistLoadLimit(yandexMusicConfig.getArtistLoadLimit()); + } } if (sourcesConfig.isFloweryTTS()) { this.flowerytts = new FloweryTTSSourceManager(floweryTTSConfig.getVoice()); diff --git a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/YandexMusicConfig.java b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/YandexMusicConfig.java index 67c51bb7..3f3dc0ff 100644 --- a/plugin/src/main/java/com/github/topi314/lavasrc/plugin/YandexMusicConfig.java +++ b/plugin/src/main/java/com/github/topi314/lavasrc/plugin/YandexMusicConfig.java @@ -8,6 +8,9 @@ public class YandexMusicConfig { private String accessToken; + private int playlistLoadLimit = 1; + private int albumLoadLimit = 1; + private int artistLoadLimit = 1; public String getAccessToken() { return this.accessToken; @@ -16,4 +19,25 @@ public String getAccessToken() { public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + + public int getPlaylistLoadLimit() { + return this.playlistLoadLimit; + } + public void setPlaylistLoadLimit(int playlistLoadLimit) { + this.playlistLoadLimit = playlistLoadLimit; + } + + public int getAlbumLoadLimit() { + return this.albumLoadLimit; + } + public void setAlbumLoadLimit(int albumLoadLimit) { + this.albumLoadLimit = albumLoadLimit; + } + + public int getArtistLoadLimit() { + return this.artistLoadLimit; + } + public void setArtistLoadLimit(int artistLoadLimit) { + this.artistLoadLimit = artistLoadLimit; + } }