diff --git a/README.md b/README.md index b6c7488..b406c09 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 + # 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: + # 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] @@ -152,39 +151,37 @@ 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: - `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_MUSIC` + - ✔ Opus formats. + - ❌ No playlist support. - `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 mix/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/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/AndroidMusic.java b/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java new file mode 100644 index 0000000..db82872 --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/clients/AndroidMusic.java @@ -0,0 +1,127 @@ +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"; + + 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 + 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 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 + @NotNull + public String getIdentifier() { + return BASE_CONFIG.getName(); + } + + @Override + 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..f6e99b3 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() @@ -41,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); } @@ -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/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/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 f8514a7..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,6 +13,7 @@ private enum ClientMapping implements ClientReference { ANDROID(Android::new), ANDROID_TESTSUITE(AndroidTestsuite::new), ANDROID_LITE(AndroidLite::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..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,6 +15,7 @@ private enum ClientMapping implements ClientReference { ANDROID(AndroidWithThumbnail::new), ANDROID_TESTSUITE(AndroidTestsuiteWithThumbnail::new), ANDROID_LITE(AndroidLiteWithThumbnail::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/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); + } +}