From f560b4821cf398321eebafdbee7d81a436df7e4b Mon Sep 17 00:00:00 2001 From: Devoxin Date: Sat, 20 Jul 2024 20:47:09 +0100 Subject: [PATCH 1/4] Add ANDROID_EMBEDDED and ANDROID_MUSIC clients. --- README.md | 69 +++++++++---------- .../youtube/clients/AndroidEmbedded.java | 65 +++++++++++++++++ .../youtube/clients/AndroidMusic.java | 69 +++++++++++++++++++ .../dev/lavalink/youtube/clients/Music.java | 2 + .../youtube/plugin/ClientProviderV3.java | 2 + .../youtube/plugin/ClientProviderV4.java | 2 + .../clients/AndroidEmbeddedWithThumbnail.java | 14 ++++ .../clients/AndroidMusicWithThumbnail.java | 14 ++++ 8 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java create mode 100644 common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java create mode 100644 v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java create mode 100644 v2/src/main/java/dev/lavalink/youtube/clients/AndroidMusicWithThumbnail.java diff --git a/README.md b/README.md index b6c7488..bc11395 100644 --- a/README.md +++ b/README.md @@ -122,25 +122,24 @@ plugins: - MUSIC - ANDROID_TESTSUITE - WEB + - TVHTML5EMBEDDED - # You can configure individual clients with the following. - # Any options or clients left unspecified will use their default values - # for those individual clients. - # Client configurations will ONLY take effect if the client is registered above, - # otherwise they are ignored. - # ---------------- WARNING ---------------- - # This part of the config is for DEMONSTRATION PURPOSES ONLY! - # Do NOT use this config before understanding what the options do. - # You do NOT need to copy this config just because it is published here. - # ---------------- WARNING ---------------- - WEB: # names are specified as they are written below under "Available Clients". - # This will disable using the WEB client for video playback. + # The below section of the config allows setting specific options for each client, such as the requests they will handle. + # If an option, or client, is unspecified, then the default option value/client values will be used instead. + # If a client is configured, but is not registered above, the options for that client will be ignored. + + # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT UNDERSTANDING WHAT IT DOES. + # WARNING!: MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. + + # Write the names of clients as they are specified under the heading "Available Clients". + WEB: + # Example: Disabling a client's playback capabilities. playback: false TVHTML5EMBEDDED: - # The below config disables everything except playback for this client. - playlistLoading: false # Disables loading of playlists and mixes for this client. - videoLoading: false # Disables loading of videos for this client (playback is still allowed). - searching: false # Disables the ability to search for videos for this client. + # Example: Configuring a client to exclusively be used for playback. + playlistLoading: false # Disables loading of playlists and mixes. + videoLoading: false # Disables loading of videos for this client (does not affect playback). + searching: false # Disables the ability to search for videos. ``` > [!IMPORTANT] @@ -159,32 +158,32 @@ so these don't need changing. Currently, the following clients are available for use: - `MUSIC` - - Provides support for searching YouTube music (`ytmsearch:`) - - **This client CANNOT be used to play tracks.** You must also register one of the - below clients for track playback. + - ✔ Provides support for searching YouTube music (`ytmsearch:`). + - ❌ Cannot be used for playback. - `WEB` + - ✔ Opus formats. - `ANDROID` - - Usage of this client is no longer advised due to the frequency at which it breaks. - As of the time of writing, this client has been broken by YouTube with no known fix. + - ❌ Heavily restricted, frequently dysfunctional. - `ANDROID_TESTSUITE` - - This client has restrictions imposed, notably: it does NOT support loading of mixes or playlists, - and it is unable to yield any supported formats when playing livestreams. - It is advised not to use this client on its own for that reason, if those features are required. + - ✔ Opus formats. + - ❌ No mix/playlist/livestream support. Advised to use in conjunction with other clients. - `ANDROID_LITE` - - This client **does not receive Opus formats** so transcoding is required. - - Similar restrictions to that of `ANDROID_TESTSUITE` except livestreams are playable. + - ❌ No Opus formats (requires transcoding). + - ❌ Restricted similarly to `ANDROID_TESTSUITE` (except livestreams are playable). +- `ANDROID_EMBEDDED` + - ✔ Opus formats. + - ❌ Restrictions currently unknown. +- `ANDROID_MUSIC` + - ✔ Opus formats. + - ❌ Restrictions currently unknown. - `MEDIA_CONNECT` - - This client **does not receive Opus formats** so transcoding is required. - - This client has restrictions imposed, including but possibly not limited to: - - Unable to load playlists. - - Unable to use search. + - ❌ No Opus formats (requires transcoding). + - ❌ No playlist/search support. - `IOS` - - This client **does not receive Opus formats**, so transcoding is required. This can - increase resource consumption. It is recommended not to use this client unless it has - the least priority (specified last), or where hardware usage is not a concern. + - ❌ No Opus formats (requires transcoding). - `TVHTML5EMBEDDED` - - This client is useful for playing age-restricted tracks. Do keep in mind that, even with this - client enabled, age-restricted tracks are **not** guaranteed to play. + - ✔ Opus formats. + - ✔ Age-restricted video playback. ## Migration from Lavaplayer's built-in YouTube source diff --git a/common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java b/common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java new file mode 100644 index 0000000..3914890 --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java @@ -0,0 +1,65 @@ +package dev.lavalink.youtube.clients; + +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import dev.lavalink.youtube.YoutubeAudioSourceManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class AndroidEmbedded extends Android { + public static ClientConfig BASE_CONFIG = new ClientConfig() + .withApiKey(Android.BASE_CONFIG.getApiKey()) + .withClientName("ANDROID_EMBEDDED_PLAYER"); + + public AndroidEmbedded() { + this(ClientOptions.DEFAULT); + } + + public AndroidEmbedded(@NotNull ClientOptions options) { + super(options, false); + } + + @Override + @NotNull + protected ClientConfig getBaseClientConfig(@NotNull HttpInterface httpInterface) { + return BASE_CONFIG.copy(); + } + + @Override + @NotNull + public String getIdentifier() { + return BASE_CONFIG.getName(); + } + + @Override + public boolean canHandleRequest(@NotNull String identifier) { + // loose check to avoid loading mixes/playlists. + return !identifier.contains("list=") && super.canHandleRequest(identifier); + } + + @Override + public AudioItem loadMix(@NotNull YoutubeAudioSourceManager source, + @NotNull HttpInterface httpInterface, + @NotNull String mixId, + @Nullable String selectedVideoId) { + // Considered returning null but an exception makes it clearer as to why a mix couldn't be loaded, + // assuming someone tries to only register this client with the source manager. + // Also, an exception will halt further loading so other source managers won't be queried. + // N.B. This client genuinely cannot load mixes for whatever reason. You can get the mix metadata + // but there are no videos in the response JSON. Weird. + throw new FriendlyException("This client cannot load mixes", Severity.COMMON, + new RuntimeException("ANDROID_EMBEDDED cannot be used to load mixes")); + } + + @Override + public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, + @NotNull HttpInterface httpInterface, + @NotNull String playlistId, + @Nullable String selectedVideoId) { + // Similar to mixes except server returns status code 500 when trying to load playlists. + throw new FriendlyException("This client cannot load playlists", Severity.COMMON, + new RuntimeException("ANDROID_EMBEDDED cannot be used to load playlists")); + } +} diff --git a/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java b/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java new file mode 100644 index 0000000..6261aa9 --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java @@ -0,0 +1,69 @@ +package dev.lavalink.youtube.clients; + +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import dev.lavalink.youtube.YoutubeAudioSourceManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class AndroidMusic extends Android { + public static String CLIENT_VERSION = "6.42.52"; + + public static ClientConfig BASE_CONFIG = new ClientConfig() + .withApiKey(Android.BASE_CONFIG.getApiKey()) + .withClientName("ANDROID_MUSIC") + .withClientField("clientVersion", CLIENT_VERSION) + .withUserAgent(String.format("com.google.android.apps.youtube.music/%s (Linux; U; Android %s) gzip", CLIENT_VERSION, ANDROID_VERSION.getOsVersion())); + + public AndroidMusic() { + this(ClientOptions.DEFAULT); + } + + public AndroidMusic(@NotNull ClientOptions options) { + super(options, false); + } + + @Override + @NotNull + protected ClientConfig getBaseClientConfig(@NotNull HttpInterface httpInterface) { + return BASE_CONFIG.copy(); + } + + @Override + @NotNull + public String getIdentifier() { + return BASE_CONFIG.getName(); + } + + @Override + public boolean canHandleRequest(@NotNull String identifier) { + // loose check to avoid loading mixes/playlists. + return !identifier.contains("list=") && super.canHandleRequest(identifier); + } + + @Override + public AudioItem loadMix(@NotNull YoutubeAudioSourceManager source, + @NotNull HttpInterface httpInterface, + @NotNull String mixId, + @Nullable String selectedVideoId) { + // Considered returning null but an exception makes it clearer as to why a mix couldn't be loaded, + // assuming someone tries to only register this client with the source manager. + // Also, an exception will halt further loading so other source managers won't be queried. + // N.B. This client genuinely cannot load mixes for whatever reason. You can get the mix metadata + // but there are no videos in the response JSON. Weird. + throw new FriendlyException("This client cannot load mixes", Severity.COMMON, + new RuntimeException("ANDROID_MUSIC cannot be used to load mixes")); + } + + @Override + public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, + @NotNull HttpInterface httpInterface, + @NotNull String playlistId, + @Nullable String selectedVideoId) { + // Similar to mixes except server returns status code 500 when trying to load playlists. + throw new FriendlyException("This client cannot load playlists", Severity.COMMON, + new RuntimeException("ANDROID_MUSIC cannot be used to load playlists")); + } +} diff --git a/common/src/main/java/dev/lavalink/youtube/clients/Music.java b/common/src/main/java/dev/lavalink/youtube/clients/Music.java index 883b5bd..6d8d287 100644 --- a/common/src/main/java/dev/lavalink/youtube/clients/Music.java +++ b/common/src/main/java/dev/lavalink/youtube/clients/Music.java @@ -31,6 +31,8 @@ public ClientConfig getBaseClientConfig(@NotNull HttpInterface httpInterface) { public String getPlayerParams() { // This client is not used for format loading so, we don't have // any player parameters attached to it. + // TODO?: This client *can* do playback, so maybe look into allowing + // this client to be used in playback rotation. throw new UnsupportedOperationException(); } diff --git a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java index f8514a7..116dfb6 100644 --- a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java +++ b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java @@ -13,6 +13,8 @@ private enum ClientMapping implements ClientReference { ANDROID(Android::new), ANDROID_TESTSUITE(AndroidTestsuite::new), ANDROID_LITE(AndroidLite::new), + ANDROID_EMBEDDED(AndroidEmbedded::new), + ANDROID_MUSIC(AndroidMusic::new), IOS(Ios::new), MUSIC(Music::new), TVHTML5EMBEDDED(TvHtml5Embedded::new), diff --git a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java index 04f3f2a..7d9b8f4 100644 --- a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java +++ b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java @@ -15,6 +15,8 @@ private enum ClientMapping implements ClientReference { ANDROID(AndroidWithThumbnail::new), ANDROID_TESTSUITE(AndroidTestsuiteWithThumbnail::new), ANDROID_LITE(AndroidLiteWithThumbnail::new), + ANDROID_EMBEDDED(AndroidEmbeddedWithThumbnail::new), + ANDROID_MUSIC(AndroidMusicWithThumbnail::new), IOS(IosWithThumbnail::new), MUSIC(MusicWithThumbnail::new), TVHTML5EMBEDDED(TvHtml5EmbeddedWithThumbnail::new), diff --git a/v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java b/v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java new file mode 100644 index 0000000..414f140 --- /dev/null +++ b/v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java @@ -0,0 +1,14 @@ +package dev.lavalink.youtube.clients; + +import dev.lavalink.youtube.clients.skeleton.NonMusicClientWithThumbnail; +import org.jetbrains.annotations.NotNull; + +public class AndroidEmbeddedWithThumbnail extends AndroidEmbedded implements NonMusicClientWithThumbnail { + public AndroidEmbeddedWithThumbnail() { + super(); + } + + public AndroidEmbeddedWithThumbnail(@NotNull ClientOptions options) { + super(options); + } +} diff --git a/v2/src/main/java/dev/lavalink/youtube/clients/AndroidMusicWithThumbnail.java b/v2/src/main/java/dev/lavalink/youtube/clients/AndroidMusicWithThumbnail.java new file mode 100644 index 0000000..387b79a --- /dev/null +++ b/v2/src/main/java/dev/lavalink/youtube/clients/AndroidMusicWithThumbnail.java @@ -0,0 +1,14 @@ +package dev.lavalink.youtube.clients; + +import dev.lavalink.youtube.clients.skeleton.NonMusicClientWithThumbnail; +import org.jetbrains.annotations.NotNull; + +public class AndroidMusicWithThumbnail extends AndroidMusic implements NonMusicClientWithThumbnail { + public AndroidMusicWithThumbnail() { + super(); + } + + public AndroidMusicWithThumbnail(@NotNull ClientOptions options) { + super(options); + } +} From 95b114979881574d5bafb7bd8ff17d75ccfd6f32 Mon Sep 17 00:00:00 2001 From: Devoxin Date: Sat, 20 Jul 2024 20:49:08 +0100 Subject: [PATCH 2/4] readme changes --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bc11395..6a881a4 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,8 @@ plugins: # If an option, or client, is unspecified, then the default option value/client values will be used instead. # If a client is configured, but is not registered above, the options for that client will be ignored. - # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT UNDERSTANDING WHAT IT DOES. - # WARNING!: MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. + # WARNING!: THE BELOW CONFIG IS FOR ILLUSTRATION PURPOSES. DO NOT COPY OR USE THIS WITHOUT + # WARNING!: UNDERSTANDING WHAT IT DOES. MISCONFIGURATION WILL HINDER YOUTUBE-SOURCE'S ABILITY TO WORK PROPERLY. # Write the names of clients as they are specified under the heading "Available Clients". WEB: @@ -151,8 +151,9 @@ lavalink: youtube: false ``` -Existing options, such as `ratelimit` and `youtubePlaylistLoadLimit` will be picked up automatically by the plugin, -so these don't need changing. +> [!NOTE] +> Existing options, such as `ratelimit` and `youtubePlaylistLoadLimit` will be picked up automatically by the plugin, +> so these don't need changing. ## Available Clients Currently, the following clients are available for use: From 63d71516bb3326779f204f02af5f6aa8265f542a Mon Sep 17 00:00:00 2001 From: Devoxin Date: Wed, 24 Jul 2024 22:00:35 +0100 Subject: [PATCH 3/4] Remove ANDROID_EMBEDDED, properly implement ANDROID_MUSIC. --- README.md | 5 +- .../youtube/YoutubeAudioSourceManager.java | 1 - .../youtube/clients/AndroidEmbedded.java | 65 ------------ .../youtube/clients/AndroidMusic.java | 98 +++++++++++++++---- .../youtube/clients/MediaConnect.java | 22 +++++ .../youtube/clients/TvHtml5Embedded.java | 5 +- .../youtube/plugin/ClientProviderV3.java | 1 - .../youtube/plugin/ClientProviderV4.java | 1 - .../clients/AndroidEmbeddedWithThumbnail.java | 14 --- 9 files changed, 105 insertions(+), 107 deletions(-) delete mode 100644 common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java delete mode 100644 v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java diff --git a/README.md b/README.md index 6a881a4..04d0227 100644 --- a/README.md +++ b/README.md @@ -171,12 +171,9 @@ Currently, the following clients are available for use: - `ANDROID_LITE` - ❌ No Opus formats (requires transcoding). - ❌ Restricted similarly to `ANDROID_TESTSUITE` (except livestreams are playable). -- `ANDROID_EMBEDDED` - - ✔ Opus formats. - - ❌ Restrictions currently unknown. - `ANDROID_MUSIC` - ✔ Opus formats. - - ❌ Restrictions currently unknown. + - ❌ No playlist support. - `MEDIA_CONNECT` - ❌ No Opus formats (requires transcoding). - ❌ No playlist/search support. diff --git a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java index 9e24f1b..76928fb 100644 --- a/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java +++ b/common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java @@ -40,7 +40,6 @@ public class YoutubeAudioSourceManager implements AudioSourceManager { // TODO: connect timeout = 16000ms, read timeout = 8000ms (as observed from scraped youtube config) // TODO: look at possibly scraping jsUrl from WEB config to save a request - // TODO: search providers use cookieless httpinterfacemanagers. should this do the same? // TODO(music): scrape config? it's identical to WEB. private static final Logger log = LoggerFactory.getLogger(YoutubeAudioSourceManager.class); diff --git a/common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java b/common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java deleted file mode 100644 index 3914890..0000000 --- a/common/src/main/java/dev/lavalink/youtube/clients/AndroidEmbedded.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.lavalink.youtube.clients; - -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import com.sedmelluq.discord.lavaplayer.track.AudioItem; -import dev.lavalink.youtube.YoutubeAudioSourceManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class AndroidEmbedded extends Android { - public static ClientConfig BASE_CONFIG = new ClientConfig() - .withApiKey(Android.BASE_CONFIG.getApiKey()) - .withClientName("ANDROID_EMBEDDED_PLAYER"); - - public AndroidEmbedded() { - this(ClientOptions.DEFAULT); - } - - public AndroidEmbedded(@NotNull ClientOptions options) { - super(options, false); - } - - @Override - @NotNull - protected ClientConfig getBaseClientConfig(@NotNull HttpInterface httpInterface) { - return BASE_CONFIG.copy(); - } - - @Override - @NotNull - public String getIdentifier() { - return BASE_CONFIG.getName(); - } - - @Override - public boolean canHandleRequest(@NotNull String identifier) { - // loose check to avoid loading mixes/playlists. - return !identifier.contains("list=") && super.canHandleRequest(identifier); - } - - @Override - public AudioItem loadMix(@NotNull YoutubeAudioSourceManager source, - @NotNull HttpInterface httpInterface, - @NotNull String mixId, - @Nullable String selectedVideoId) { - // Considered returning null but an exception makes it clearer as to why a mix couldn't be loaded, - // assuming someone tries to only register this client with the source manager. - // Also, an exception will halt further loading so other source managers won't be queried. - // N.B. This client genuinely cannot load mixes for whatever reason. You can get the mix metadata - // but there are no videos in the response JSON. Weird. - throw new FriendlyException("This client cannot load mixes", Severity.COMMON, - new RuntimeException("ANDROID_EMBEDDED cannot be used to load mixes")); - } - - @Override - public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, - @NotNull HttpInterface httpInterface, - @NotNull String playlistId, - @Nullable String selectedVideoId) { - // Similar to mixes except server returns status code 500 when trying to load playlists. - throw new FriendlyException("This client cannot load playlists", Severity.COMMON, - new RuntimeException("ANDROID_EMBEDDED cannot be used to load playlists")); - } -} diff --git a/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java b/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java index 6261aa9..db82872 100644 --- a/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java +++ b/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java @@ -1,13 +1,21 @@ package dev.lavalink.youtube.clients; +import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; +import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; +import com.sedmelluq.discord.lavaplayer.tools.Units; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import dev.lavalink.youtube.YoutubeAudioSourceManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + public class AndroidMusic extends Android { public static String CLIENT_VERSION = "6.42.52"; @@ -33,36 +41,86 @@ protected ClientConfig getBaseClientConfig(@NotNull HttpInterface httpInterface) @Override @NotNull - public String getIdentifier() { - return BASE_CONFIG.getName(); + protected JsonBrowser extractMixPlaylistData(@NotNull JsonBrowser json) { + return json.get("contents") + .get("singleColumnMusicWatchNextResultsRenderer") + .get("tabbedRenderer") + .get("watchNextTabbedResultsRenderer") + .get("tabs") + .values() + .stream() + .filter(tab -> "Up next".equalsIgnoreCase(tab.get("tabRenderer").get("title").text())) + .findFirst() + .orElse(json) + .get("tabRenderer") + .get("content") + .get("musicQueueRenderer") + .get("content") + .get("playlistPanelRenderer"); + } + + @NotNull + protected List extractSearchResults(@NotNull YoutubeAudioSourceManager source, + @NotNull JsonBrowser json) { + return json.get("contents") + .get("tabbedSearchResultsRenderer") + .get("tabs") + .values() + .stream() + .flatMap(item -> item.get("tabRenderer").get("content").get("sectionListRenderer").get("contents").values().stream()) + .map(item -> extractAudioTrack(item.get("musicCardShelfRenderer"), source)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Override + @Nullable + protected AudioTrack extractAudioTrack(@NotNull JsonBrowser json, @NotNull YoutubeAudioSourceManager source) { + if (json.isNull() || !json.get("unplayableText").isNull()) return null; + + AudioTrack track = super.extractAudioTrack(json, source); + + if (track != null) { + return track; + } + + String videoId = json.get("onTap").get("watchEndpoint").get("videoId").text(); + + if (videoId == null) { + return null; + } + + JsonBrowser titleJson = json.get("title"); + JsonBrowser secondaryJson = json.get("menu").get("menuRenderer").get("title").get("musicMenuTitleRenderer").get("secondaryText").get("runs"); + String title = DataFormatTools.defaultOnNull(titleJson.get("runs").index(0).get("text").text(), titleJson.get("simpleText").text()); + String author = secondaryJson.index(0).get("text").text(); + + JsonBrowser durationJson = secondaryJson.index(2); + String durationText = DataFormatTools.defaultOnNull(durationJson.get("text").text(), durationJson.get("runs").index(0).get("text").text()); + + long duration = DataFormatTools.durationTextToMillis(durationText); + return buildAudioTrack(source, json, title, author, duration, videoId, false); } @Override public boolean canHandleRequest(@NotNull String identifier) { - // loose check to avoid loading mixes/playlists. - return !identifier.contains("list=") && super.canHandleRequest(identifier); + // loose check to avoid loading playlists. + // this client does support them, but it seems to be missing fields (i.e. videoId) + return (!identifier.contains("list=") || identifier.contains("list=RD")) && super.canHandleRequest(identifier); } @Override - public AudioItem loadMix(@NotNull YoutubeAudioSourceManager source, - @NotNull HttpInterface httpInterface, - @NotNull String mixId, - @Nullable String selectedVideoId) { - // Considered returning null but an exception makes it clearer as to why a mix couldn't be loaded, - // assuming someone tries to only register this client with the source manager. - // Also, an exception will halt further loading so other source managers won't be queried. - // N.B. This client genuinely cannot load mixes for whatever reason. You can get the mix metadata - // but there are no videos in the response JSON. Weird. - throw new FriendlyException("This client cannot load mixes", Severity.COMMON, - new RuntimeException("ANDROID_MUSIC cannot be used to load mixes")); + @NotNull + public String getIdentifier() { + return BASE_CONFIG.getName(); } @Override - public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, - @NotNull HttpInterface httpInterface, - @NotNull String playlistId, - @Nullable String selectedVideoId) { - // Similar to mixes except server returns status code 500 when trying to load playlists. + public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, @NotNull HttpInterface httpInterface, @NotNull String playlistId, @Nullable String selectedVideoId) { + // It does actually return JSON, but it seems like videoId is missing. + // Each video JSON contains a "Content is unavailable" message. + // Theoretically, you could construct an audio track from the JSON as author, duration and title are there. + // Video ID is included in the thumbnail URL, but I don't think it's worth writing parsing for. throw new FriendlyException("This client cannot load playlists", Severity.COMMON, new RuntimeException("ANDROID_MUSIC cannot be used to load playlists")); } diff --git a/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java b/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java index 3d7e2f8..7f332ff 100644 --- a/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java +++ b/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java @@ -1,9 +1,13 @@ package dev.lavalink.youtube.clients; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; import dev.lavalink.youtube.YoutubeAudioSourceManager; import dev.lavalink.youtube.clients.skeleton.StreamingNonMusicClient; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class MediaConnect extends StreamingNonMusicClient { public static ClientConfig BASE_CONFIG = new ClientConfig() @@ -50,4 +54,22 @@ public boolean canHandleRequest(@NotNull String identifier) { public String getIdentifier() { return BASE_CONFIG.getName(); } + + @Override + public AudioItem loadSearch(@NotNull YoutubeAudioSourceManager source, @NotNull HttpInterface httpInterface, @NotNull String searchQuery) { + throw new FriendlyException("This client cannot load searches", Severity.COMMON, + new RuntimeException("MEDIA_CONNECT cannot be used to load searches")); + } + + @Override + public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, @NotNull HttpInterface httpInterface, @NotNull String playlistId, @Nullable String selectedVideoId) { + throw new FriendlyException("This client cannot load playlists", Severity.COMMON, + new RuntimeException("MEDIA_CONNECT cannot be used to load playlists")); + } + + @Override + public AudioItem loadMix(@NotNull YoutubeAudioSourceManager source, @NotNull HttpInterface httpInterface, @NotNull String mixId, @Nullable String selectedVideoId) { + throw new FriendlyException("This client cannot load mixes", Severity.COMMON, + new RuntimeException("MEDIA_CONNECT cannot be used to load mixes")); + } } diff --git a/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java b/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java index 34a7ec6..9c6c86e 100644 --- a/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java +++ b/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java @@ -1,6 +1,8 @@ package dev.lavalink.youtube.clients; import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.Units; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; @@ -110,6 +112,7 @@ public AudioItem loadPlaylist(@NotNull YoutubeAudioSourceManager source, @NotNull HttpInterface httpInterface, @NotNull String playlistId, @Nullable String selectedVideoId) { - throw new UnsupportedOperationException(); + throw new FriendlyException("This client cannot load playlists", Severity.COMMON, + new RuntimeException("TVHTML5_EMBEDDED cannot be used to load playlists")); } } diff --git a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java index 116dfb6..56fe1fc 100644 --- a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java +++ b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java @@ -13,7 +13,6 @@ private enum ClientMapping implements ClientReference { ANDROID(Android::new), ANDROID_TESTSUITE(AndroidTestsuite::new), ANDROID_LITE(AndroidLite::new), - ANDROID_EMBEDDED(AndroidEmbedded::new), ANDROID_MUSIC(AndroidMusic::new), IOS(Ios::new), MUSIC(Music::new), diff --git a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java index 7d9b8f4..0333081 100644 --- a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java +++ b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java @@ -15,7 +15,6 @@ private enum ClientMapping implements ClientReference { ANDROID(AndroidWithThumbnail::new), ANDROID_TESTSUITE(AndroidTestsuiteWithThumbnail::new), ANDROID_LITE(AndroidLiteWithThumbnail::new), - ANDROID_EMBEDDED(AndroidEmbeddedWithThumbnail::new), ANDROID_MUSIC(AndroidMusicWithThumbnail::new), IOS(IosWithThumbnail::new), MUSIC(MusicWithThumbnail::new), diff --git a/v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java b/v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java deleted file mode 100644 index 414f140..0000000 --- a/v2/src/main/java/dev/lavalink/youtube/clients/AndroidEmbeddedWithThumbnail.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.lavalink.youtube.clients; - -import dev.lavalink.youtube.clients.skeleton.NonMusicClientWithThumbnail; -import org.jetbrains.annotations.NotNull; - -public class AndroidEmbeddedWithThumbnail extends AndroidEmbedded implements NonMusicClientWithThumbnail { - public AndroidEmbeddedWithThumbnail() { - super(); - } - - public AndroidEmbeddedWithThumbnail(@NotNull ClientOptions options) { - super(options); - } -} From d854f62a84880350c0d82e8c0d1e660058765127 Mon Sep 17 00:00:00 2001 From: Devoxin Date: Wed, 24 Jul 2024 22:05:35 +0100 Subject: [PATCH 4/4] clarify MEDIA_CONNECT does not receive mix videos --- README.md | 2 +- .../main/java/dev/lavalink/youtube/clients/MediaConnect.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 04d0227..b406c09 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ Currently, the following clients are available for use: - ❌ No playlist support. - `MEDIA_CONNECT` - ❌ No Opus formats (requires transcoding). - - ❌ No playlist/search support. + - ❌ No mix/playlist/search support. - `IOS` - ❌ No Opus formats (requires transcoding). - `TVHTML5EMBEDDED` diff --git a/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java b/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java index 7f332ff..f6e99b3 100644 --- a/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java +++ b/common/src/main/java/dev/lavalink/youtube/clients/MediaConnect.java @@ -45,7 +45,7 @@ public ClientOptions getOptions() { @Override public boolean canHandleRequest(@NotNull String identifier) { // This client appears to be able to load livestreams and videos, but will - // receive 400 bad request when loading playlists. + // receive 400 bad request when loading playlists. mixes do return JSON, but does not contain mix videos. return !identifier.startsWith(YoutubeAudioSourceManager.SEARCH_PREFIX) && !identifier.contains("list=") && super.canHandleRequest(identifier); }