Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
126 changes: 126 additions & 0 deletions common/src/main/java/dev/lavalink/youtube/clients/TvHtml5Embedded.java
Original file line number Diff line number Diff line change
@@ -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<AudioTrack> 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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}