diff --git a/README.md b/README.md index 982d8936..83e56bab 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ Currently, the following clients are available for use: | `ANDROID_VR` | Yes | No | No | Yes + Livestream | Video, Search, Playlist, Mix | | | `IOS` | No | No | No | Yes + Livestream | Video, Search, Playlist, Mix | | | `TV` | Yes | Yes | With OAuth | Yes + Livestream | None | Playback requires sign-in | +| `TVHTML5EMBEDDED` | No | Yes | With OAuth | No | Video, Search, Mix | Playback no longer works, loading does. | > [!NOTE] > Clients that do not return Opus formats will require transcoding. diff --git a/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java b/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java new file mode 100644 index 00000000..a369fead --- /dev/null +++ b/common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java @@ -0,0 +1,126 @@ +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 dev.lavalink.youtube.clients.skeleton.StreamingNonMusicClient; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class TvHtml5Embedded extends StreamingNonMusicClient { + public static ClientConfig BASE_CONFIG = new ClientConfig() + .withClientName("TVHTML5_SIMPLY_EMBEDDED_PLAYER") + .withClientField("clientVersion", "2.0") + .withThirdPartyEmbedUrl("https://www.youtube.com"); + + protected ClientOptions options; + + public TvHtml5Embedded() { + this(ClientOptions.DEFAULT); + } + + public TvHtml5Embedded(@NotNull ClientOptions options) { + this.options = options; + } + + @Override + @NotNull + protected ClientConfig getBaseClientConfig(@NotNull HttpInterface httpInterface) { + return BASE_CONFIG.copy(); + } + + @Override + @NotNull + protected JsonBrowser extractPlaylistVideoList(@NotNull JsonBrowser json) { + return json.get("contents") + .get("sectionListRenderer") + .get("contents") + .index(0) + .get("playlistVideoListRenderer"); + } + + @Override + protected void extractPlaylistTracks(@NotNull JsonBrowser json, + @NotNull List tracks, + @NotNull YoutubeAudioSourceManager source) { + if (!json.get("contents").isNull()) { + json = json.get("contents"); + } + + if (json.isNull()) { + return; + } + + for (JsonBrowser track : json.values()) { + JsonBrowser item = track.get("videoRenderer"); + JsonBrowser authorJson = item.get("shortBylineText"); + + // this client doesn't appear to receive "isPlayable" fields. + // author is null -> video is region blocked + if (!authorJson.isNull()) { + String videoId = item.get("videoId").text(); + JsonBrowser titleField = item.get("title"); + String title = DataFormatTools.defaultOnNull(titleField.get("simpleText").text(), titleField.get("runs").index(0).get("text").text()); + String author = DataFormatTools.defaultOnNull(authorJson.get("runs").index(0).get("text").text(), "Unknown artist"); + long duration = Units.secondsToMillis(item.get("lengthSeconds").asLong(Units.DURATION_SEC_UNKNOWN)); + tracks.add(buildAudioTrack(source, track, title, author, duration, videoId, false)); + } + } + } + + @Override + public boolean isEmbedded() { + return true; + } + + @Override + @NotNull + public String getPlayerParams() { + return WEB_PLAYER_PARAMS; + } + + @Override + @NotNull + public ClientOptions getOptions() { + return this.options; + } + + @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 + // that could be the difference between playable and unplayable -- + // notably the "isPlayable" field. + // I'm also cautious of routing a lot of traffic through this client. + // There is overridden code above but that's mostly just for reference. + return (!identifier.contains("list=") || identifier.contains("list=RD")) && super.canHandleRequest(identifier); + } + + @Override + public boolean supportsOAuth() { + return true; + } + + @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) { + 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 60d22a1f..2a9d7853 100644 --- a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java +++ b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV3.java @@ -16,6 +16,7 @@ private enum ClientMapping implements ClientReference { IOS(Ios::new), MUSIC(Music::new), TV(Tv::new), + TVHTML5EMBEDDED(TvHtml5Embedded::new), WEB(Web::new), WEBEMBEDDED(WebEmbedded::new), MWEB(MWeb::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 0e7dae83..5c44f2dd 100644 --- a/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java +++ b/plugin/src/main/java/dev/lavalink/youtube/plugin/ClientProviderV4.java @@ -16,6 +16,7 @@ private enum ClientMapping implements ClientReference { IOS(IosWithThumbnail::new), MUSIC(MusicWithThumbnail::new), TV(Tv::new), // This has no WithThumbnail companion as it's a playback-only client. + TVHTML5EMBEDDED(TvHtml5EmbeddedWithThumbnail::new), WEB(WebWithThumbnail::new), WEBEMBEDDED(WebEmbeddedWithThumbnail::new), MWEB(MWebWithThumbnail::new); diff --git a/v2/src/main/java/dev/lavalink/youtube/clients/TvHtml5EmbeddedWithThumbnail.java b/v2/src/main/java/dev/lavalink/youtube/clients/TvHtml5EmbeddedWithThumbnail.java new file mode 100644 index 00000000..feb15449 --- /dev/null +++ b/v2/src/main/java/dev/lavalink/youtube/clients/TvHtml5EmbeddedWithThumbnail.java @@ -0,0 +1,14 @@ +package dev.lavalink.youtube.clients; + +import dev.lavalink.youtube.clients.skeleton.NonMusicClientWithThumbnail; +import org.jetbrains.annotations.NotNull; + +public class TvHtml5EmbeddedWithThumbnail extends TvHtml5Embedded implements NonMusicClientWithThumbnail { + public TvHtml5EmbeddedWithThumbnail() { + super(); + } + + public TvHtml5EmbeddedWithThumbnail(@NotNull ClientOptions options) { + super(options); + } +}