Implement support for channel tabs.

This commit is contained in:
Kavin 2022-10-27 11:55:44 +01:00
parent 2cc9631856
commit 00a6d7da93
No known key found for this signature in database
GPG Key ID: 49451E4482CC5BCD
20 changed files with 249 additions and 214 deletions

View File

@ -135,6 +135,16 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(GET, "/channels/tabs", AsyncServlet.ofBlocking(executor, request -> {
try {
String nextpage = request.getQueryParameter("nextpage");
if (StringUtils.isEmpty(nextpage))
return getJsonResponse(ResponseHelper.channelTabResponse(request.getQueryParameter("data")), "public, max-age=3600", true);
else
return getJsonResponse(ResponseHelper.channelTabPageResponse(request.getQueryParameter("data"), nextpage), "public, max-age=3600", true);
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(GET, "/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> {
try {
var playlistId = request.getPathParameter("playlistId");

View File

@ -1,6 +1,7 @@
package me.kavin.piped.consts;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import me.kavin.piped.utils.PageMixin;
@ -69,7 +70,9 @@ public class Constants {
public static final String VERSION;
public static final ObjectMapper mapper = new ObjectMapper().addMixIn(Page.class, PageMixin.class);
public static final ObjectMapper mapper = JsonMapper.builder()
.addMixIn(Page.class, PageMixin.class)
.build();
public static final Object2ObjectOpenHashMap<String, String> hibernateProperties = new Object2ObjectOpenHashMap<>();

View File

@ -19,8 +19,6 @@ import me.kavin.piped.utils.obj.Channel;
import me.kavin.piped.utils.obj.Playlist;
import me.kavin.piped.utils.obj.*;
import me.kavin.piped.utils.obj.db.*;
import me.kavin.piped.utils.obj.search.SearchChannel;
import me.kavin.piped.utils.obj.search.SearchPlaylist;
import me.kavin.piped.utils.resp.*;
import okhttp3.FormBody;
import okhttp3.Request;
@ -34,6 +32,7 @@ import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -45,6 +44,7 @@ import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YouTubeChannelTabHandler;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
@ -158,12 +158,7 @@ public class ResponseHelper {
stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec())));
}
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
info.getRelatedItems().forEach(o -> {
if (o instanceof StreamInfoItem)
relatedStreams.add(collectRelatedStream(o));
});
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
long time = info.getUploadDate() != null ? info.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
: System.currentTimeMillis();
@ -224,9 +219,7 @@ public class ResponseHelper {
final ChannelInfo info = ChannelInfo.getInfo("https://youtube.com/" + channelPath);
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o)));
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
Multithreading.runAsync(() -> {
@ -292,9 +285,19 @@ public class ResponseHelper {
nextpage = mapper.writeValueAsString(page);
}
List<ChannelTab> tabs = info.getTabs()
.stream()
.map(tab -> {
try {
return new ChannelTab(tab.getTab().name(), mapper.writeValueAsString(tab));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}).toList();
final Channel channel = new Channel(info.getId(), info.getName(), rewriteURL(info.getAvatarUrl()),
rewriteURL(info.getBannerUrl()), info.getDescription(), info.getSubscriberCount(), info.isVerified(),
nextpage, relatedStreams);
nextpage, relatedStreams, tabs);
IPFS.publishData(channel);
@ -313,9 +316,7 @@ public class ResponseHelper {
InfoItemsPage<StreamInfoItem> info = ChannelInfo.getMoreItems(YOUTUBE_SERVICE,
"https://youtube.com/channel/" + channelId, prevpage);
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
info.getItems().forEach(o -> relatedStreams.add(collectRelatedStream(o)));
final List<ContentItem> relatedStreams = collectRelatedItems(info.getItems());
String nextpage = null;
if (info.hasNextPage()) {
@ -329,13 +330,49 @@ public class ResponseHelper {
}
public static byte[] channelTabResponse(String data)
throws IOException, ExtractionException {
if (StringUtils.isEmpty(data))
return mapper.writeValueAsBytes(new InvalidRequestResponse());
YouTubeChannelTabHandler tabHandler = mapper.readValue(data, YouTubeChannelTabHandlerMixin.class);
var info = ChannelTabInfo.getInfo(YOUTUBE_SERVICE, tabHandler);
List<ContentItem> items = collectRelatedItems(info.getRelatedItems());
String nextpage = null;
if (info.hasNextPage()) {
Page page = info.getNextPage();
nextpage = mapper.writeValueAsString(page);
}
return mapper.writeValueAsBytes(new ChannelTabData(nextpage, items));
}
public static byte[] channelTabPageResponse(String data, String nextpage) throws Exception {
if (StringUtils.isEmpty(data))
return mapper.writeValueAsBytes(new InvalidRequestResponse());
YouTubeChannelTabHandler tabHandler = mapper.readValue(data, YouTubeChannelTabHandlerMixin.class);
Page nextPage = mapper.readValue(nextpage, Page.class);
var info = ChannelTabInfo.getMoreItems(YOUTUBE_SERVICE, tabHandler, nextPage);
List<ContentItem> items = collectRelatedItems(info.getItems());
return mapper.writeValueAsBytes(new ChannelTabData(null, items));
}
public static byte[] trendingResponse(String region)
throws ExtractionException, IOException {
if (region == null)
return mapper.writeValueAsBytes(new InvalidRequestResponse());
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
KioskList kioskList = YOUTUBE_SERVICE.getKioskList();
kioskList.forceContentCountry(new ContentCountry(region));
@ -343,7 +380,7 @@ public class ResponseHelper {
extractor.fetchPage();
KioskInfo info = KioskInfo.getInfo(extractor);
info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o)));
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
return mapper.writeValueAsBytes(relatedStreams);
}
@ -376,7 +413,7 @@ public class ResponseHelper {
return mapper.writeValueAsBytes(mapper.createObjectNode()
.put("error", "Playlist not found"));
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
final List<ContentItem> relatedStreams = new ObjectArrayList<>();
var videos = pl.getVideos();
@ -399,9 +436,7 @@ public class ResponseHelper {
final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId);
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o)));
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
String nextpage = null;
if (info.hasNextPage()) {
@ -430,9 +465,7 @@ public class ResponseHelper {
InfoItemsPage<StreamInfoItem> info = PlaylistInfo.getMoreItems(YOUTUBE_SERVICE,
"https://www.youtube.com/playlist?list=" + playlistId, prevpage);
final List<StreamItem> relatedStreams = new ObjectArrayList<>();
info.getItems().forEach(o -> relatedStreams.add(collectRelatedStream(o)));
final List<ContentItem> relatedStreams = collectRelatedItems(info.getItems());
String nextpage = null;
if (info.hasNextPage()) {
@ -556,24 +589,7 @@ public class ResponseHelper {
final SearchInfo info = SearchInfo.getInfo(YOUTUBE_SERVICE,
YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null));
ObjectArrayList<Object> items = new ObjectArrayList<>();
info.getRelatedItems().forEach(item -> {
switch (item.getInfoType()) {
case STREAM -> items.add(collectRelatedStream(item));
case CHANNEL -> {
ChannelInfoItem channel = (ChannelInfoItem) item;
items.add(new SearchChannel(item.getName(), rewriteURL(item.getThumbnailUrl()),
substringYouTube(item.getUrl()), channel.getDescription(), channel.getSubscriberCount(),
channel.getStreamCount(), channel.isVerified()));
}
case PLAYLIST -> {
PlaylistInfoItem playlist = (PlaylistInfoItem) item;
items.add(new SearchPlaylist(item.getName(), rewriteURL(item.getThumbnailUrl()),
substringYouTube(item.getUrl()), playlist.getUploaderName(), playlist.getStreamCount()));
}
}
});
List<ContentItem> items = collectRelatedItems(info.getRelatedItems());
Page nextpage = info.getNextPage();
@ -593,24 +609,7 @@ public class ResponseHelper {
InfoItemsPage<InfoItem> pages = SearchInfo.getMoreItems(YOUTUBE_SERVICE,
YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null), prevpage);
ObjectArrayList<Object> items = new ObjectArrayList<>();
pages.getItems().forEach(item -> {
switch (item.getInfoType()) {
case STREAM -> items.add(collectRelatedStream(item));
case CHANNEL -> {
ChannelInfoItem channel = (ChannelInfoItem) item;
items.add(new SearchChannel(item.getName(), rewriteURL(item.getThumbnailUrl()),
substringYouTube(item.getUrl()), channel.getDescription(), channel.getSubscriberCount(),
channel.getStreamCount(), channel.isVerified()));
}
case PLAYLIST -> {
PlaylistInfoItem playlist = (PlaylistInfoItem) item;
items.add(new SearchPlaylist(item.getName(), rewriteURL(item.getThumbnailUrl()),
substringYouTube(item.getUrl()), playlist.getUploaderName(), playlist.getStreamCount()));
}
}
});
List<ContentItem> items = collectRelatedItems(pages.getItems());
Page nextpage = pages.getNextPage();
@ -1754,31 +1753,47 @@ public class ResponseHelper {
formBuilder.add("hub.mode", "subscribe");
formBuilder.add("hub.lease_seconds", "432000");
var resp = Constants.h2client
try (var resp = Constants.h2client
.newCall(builder.post(formBuilder.build())
.build()).execute();
.build()).execute()) {
if (resp.code() == 202) {
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
var tr = s.beginTransaction();
if (pubsub == null) {
pubsub = new PubSub(channelId, System.currentTimeMillis());
s.insert(pubsub);
} else {
pubsub.setSubbedAt(System.currentTimeMillis());
s.update(pubsub);
if (resp.code() == 202) {
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
var tr = s.beginTransaction();
if (pubsub == null) {
pubsub = new PubSub(channelId, System.currentTimeMillis());
s.insert(pubsub);
} else {
pubsub.setSubbedAt(System.currentTimeMillis());
s.update(pubsub);
}
tr.commit();
}
tr.commit();
}
} else
System.out.println("Failed to subscribe: " + resp.code() + "\n" + Objects.requireNonNull(resp.body()).string());
} else
System.out.println("Failed to subscribe: " + resp.code() + "\n" + Objects.requireNonNull(resp.body()).string());
resp.close();
}
}
}
private static List<ContentItem> collectRelatedItems(List<? extends InfoItem> items) {
return items
.stream()
.parallel()
.map(item -> {
if (item instanceof StreamInfoItem)
return collectRelatedStream(item);
else if (item instanceof PlaylistInfoItem)
return collectRelatedPlaylist(item);
else if (item instanceof ChannelInfoItem)
return collectRelatedChannel(item);
else
throw new RuntimeException("Unknown item type: " + item.getClass().getName());
}).toList();
}
private static StreamItem collectRelatedStream(Object o) {
StreamInfoItem item = (StreamInfoItem) o;
@ -1788,4 +1803,20 @@ public class ResponseHelper {
rewriteURL(item.getUploaderAvatarUrl()), item.getTextualUploadDate(), item.getShortDescription(), item.getDuration(),
item.getViewCount(), item.getUploadDate() != null ? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : -1, item.isUploaderVerified(), item.isShortFormContent());
}
private static PlaylistItem collectRelatedPlaylist(Object o) {
PlaylistInfoItem item = (PlaylistInfoItem) o;
return new PlaylistItem(substringYouTube(item.getUrl()), item.getName(), rewriteURL(item.getThumbnailUrl()),
item.getUploaderName(), item.getPlaylistType().name(), item.getStreamCount());
}
private static ChannelItem collectRelatedChannel(Object o) {
ChannelInfoItem item = (ChannelInfoItem) o;
return new ChannelItem(substringYouTube(item.getUrl()), item.getName(), rewriteURL(item.getThumbnailUrl()),
item.getDescription(), item.getSubscriberCount(), item.getStreamCount(), item.isVerified());
}
}

View File

@ -0,0 +1,23 @@
package me.kavin.piped.utils;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YouTubeChannelTabHandler;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class YouTubeChannelTabHandlerMixin extends YouTubeChannelTabHandler {
@JsonCreator
@JsonIgnoreProperties(ignoreUnknown = true)
public YouTubeChannelTabHandlerMixin(@JsonProperty("originalUrl") String originalUrl, @JsonProperty("url") String url,
@JsonProperty("id") String id, @JsonProperty("contentFilters") List<String> contentFilters,
@JsonProperty("sortFilter") String sortFilter, @JsonProperty("tab") ChannelTabHandler.Tab tab,
@JsonProperty("visitorData") String visitorData) {
super(new ListLinkHandler(originalUrl, url, id, contentFilters, sortFilter), tab, visitorData);
}
}

View File

@ -7,10 +7,12 @@ public class Channel {
public String id, name, avatarUrl, bannerUrl, description, nextpage;
public long subscriberCount;
public boolean verified;
public List<StreamItem> relatedStreams;
public List<ContentItem> relatedStreams;
public List<ChannelTab> tabs;
public Channel(String id, String name, String avatarUrl, String bannerUrl, String description, long subscriberCount,
boolean verified, String nextpage, List<StreamItem> relatedStreams) {
boolean verified, String nextpage, List<ContentItem> relatedStreams, List<ChannelTab> tabs) {
this.id = id;
this.name = name;
this.avatarUrl = avatarUrl;
@ -20,5 +22,6 @@ public class Channel {
this.verified = verified;
this.nextpage = nextpage;
this.relatedStreams = relatedStreams;
this.tabs = tabs;
}
}

View File

@ -0,0 +1,23 @@
package me.kavin.piped.utils.obj;
public class ChannelItem extends ContentItem {
public final String type = "channel";
public String name;
public String thumbnail;
public String description;
public long subscribers, videos;
public boolean verified;
public ChannelItem(String url, String name, String thumbnail, String description, long subscribers, long videos,
boolean verified) {
super(url);
this.name = name;
this.thumbnail = thumbnail;
this.description = description;
this.subscribers = subscribers;
this.videos = videos;
this.verified = verified;
}
}

View File

@ -0,0 +1,12 @@
package me.kavin.piped.utils.obj;
public class ChannelTab {
public String name;
public String data;
public ChannelTab(String name, String data) {
this.name = name;
this.data = data;
}
}

View File

@ -0,0 +1,14 @@
package me.kavin.piped.utils.obj;
import java.util.List;
public class ChannelTabData {
public String nextpage;
public List<ContentItem> content;
public ChannelTabData(String nextpage, List<ContentItem> content) {
this.nextpage = nextpage;
this.content = content;
}
}

View File

@ -0,0 +1,10 @@
package me.kavin.piped.utils.obj;
public class ContentItem {
public String url;
public ContentItem(String url) {
this.url = url;
}
}

View File

@ -6,10 +6,10 @@ public class Playlist {
public String name, thumbnailUrl, bannerUrl, nextpage, uploader, uploaderUrl, uploaderAvatar;
public int videos;
public List<StreamItem> relatedStreams;
public List<ContentItem> relatedStreams;
public Playlist(String name, String thumbnailUrl, String bannerUrl, String nextpage, String uploader,
String uploaderUrl, String uploaderAvatar, int videos, List<StreamItem> relatedStreams) {
String uploaderUrl, String uploaderAvatar, int videos, List<ContentItem> relatedStreams) {
this.name = name;
this.thumbnailUrl = thumbnailUrl;
this.bannerUrl = bannerUrl;

View File

@ -0,0 +1,23 @@
package me.kavin.piped.utils.obj;
public class PlaylistItem extends ContentItem {
public final String type = "playlist";
public String name;
public String thumbnail;
public String uploaderName;
public String playlistType;
public long videos;
public PlaylistItem(String url, String name, String thumbnail, String uploaderName, String playlistType, long videos) {
super(url);
this.name = name;
this.thumbnail = thumbnail;
this.uploaderName = uploaderName;
this.playlistType = playlistType;
this.videos = videos;
}
}

View File

@ -1,19 +1,19 @@
package me.kavin.piped.utils.obj;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.List;
public class SearchResults {
public ObjectArrayList<Object> items;
public List<ContentItem> items;
public String nextpage, suggestion;
public boolean corrected;
public SearchResults(ObjectArrayList<Object> items, String nextpage) {
public SearchResults(List<ContentItem> items, String nextpage) {
this.nextpage = nextpage;
this.items = items;
}
public SearchResults(ObjectArrayList<Object> items, String nextpage, String suggestion, boolean corrected) {
public SearchResults(List<ContentItem> items, String nextpage, String suggestion, boolean corrected) {
this.items = items;
this.nextpage = nextpage;
this.suggestion = suggestion;

View File

@ -1,16 +1,16 @@
package me.kavin.piped.utils.obj;
public class StreamItem {
public class StreamItem extends ContentItem {
private final String type = "video";
public final String type = "stream";
public String url, title, thumbnail, uploaderName, uploaderUrl, uploaderAvatar, uploadedDate, shortDescription;
public String title, thumbnail, uploaderName, uploaderUrl, uploaderAvatar, uploadedDate, shortDescription;
public long duration, views, uploaded;
public boolean uploaderVerified, isShort;
public StreamItem(String url, String title, String thumbnail, String uploaderName, String uploaderUrl,
String uploaderAvatar, String uploadedDate, String shortDescription, long duration, long views, long uploaded, boolean uploaderVerified, boolean isShort) {
this.url = url;
super(url);
this.title = title;
this.thumbnail = thumbnail;
this.uploaderName = uploaderName;

View File

@ -15,7 +15,7 @@ public class Streams {
public List<PipedStream> audioStreams, videoStreams;
public List<StreamItem> relatedStreams;
public List<ContentItem> relatedStreams;
public List<Subtitle> subtitles;
@ -28,7 +28,7 @@ public class Streams {
public Streams(String title, String description, String uploadDate, String uploader, String uploaderUrl,
String uploaderAvatar, String thumbnailUrl, long duration, long views, long likes, long dislikes, long uploaderSubscriberCount,
boolean uploaderVerified, List<PipedStream> audioStreams, List<PipedStream> videoStreams,
List<StreamItem> relatedStreams, List<Subtitle> subtitles, boolean livestream, String hls, String dash,
List<ContentItem> relatedStreams, List<Subtitle> subtitles, boolean livestream, String hls, String dash,
String lbryId, List<ChapterSegment> chapters) {
this.title = title;
this.description = description;

View File

@ -5,9 +5,9 @@ import java.util.List;
public class StreamsPage {
public String nextpage;
public List<StreamItem> relatedStreams;
public List<ContentItem> relatedStreams;
public StreamsPage(String nextpage, List<StreamItem> relatedStreams) {
public StreamsPage(String nextpage, List<ContentItem> relatedStreams) {
this.nextpage = nextpage;
this.relatedStreams = relatedStreams;
}

View File

@ -1,33 +0,0 @@
package me.kavin.piped.utils.obj.search;
public class SearchChannel extends SearchItem {
private String description;
private long subscribers, videos;
private boolean verified;
public SearchChannel(String name, String thumbnail, String url, String description, long subscribers, long videos,
boolean verified) {
super(name, thumbnail, url);
this.description = description;
this.subscribers = subscribers;
this.videos = videos;
this.verified = verified;
}
public String getDescription() {
return description;
}
public long getSubscribers() {
return subscribers;
}
public long getVideos() {
return videos;
}
public boolean isVerified() {
return verified;
}
}

View File

@ -1,24 +0,0 @@
package me.kavin.piped.utils.obj.search;
public class SearchItem {
private String name, thumbnail, url;
public SearchItem(String name, String thumbnail, String url) {
this.name = name;
this.thumbnail = thumbnail;
this.url = url;
}
public String getName() {
return name;
}
public String getThumbnail() {
return thumbnail;
}
public String getUrl() {
return url;
}
}

View File

@ -1,21 +0,0 @@
package me.kavin.piped.utils.obj.search;
public class SearchPlaylist extends SearchItem {
private String uploaderName;
private long videos;
public SearchPlaylist(String name, String thumbnail, String url, String uploaderName, long videos) {
super(name, thumbnail, url);
this.uploaderName = uploaderName;
this.videos = videos;
}
public String getUploaderName() {
return uploaderName;
}
public long getVideos() {
return videos;
}
}

View File

@ -1,43 +0,0 @@
package me.kavin.piped.utils.obj.search;
public class SearchStream extends SearchItem {
private String uploadDate, uploader, uploaderUrl;
private long views, duration;
private boolean uploaderVerified;
public SearchStream(String name, String thumbnail, String url, String uploadDate, String uploader,
String uploaderUrl, long views, long duration, boolean uploaderVerified) {
super(name, thumbnail, url);
this.uploadDate = uploadDate;
this.uploader = uploader;
this.uploaderUrl = uploaderUrl;
this.views = views;
this.duration = duration;
this.uploaderVerified = uploaderVerified;
}
public String getUploadDate() {
return uploadDate;
}
public String getUploader() {
return uploader;
}
public String getUploaderUrl() {
return uploaderUrl;
}
public long getViews() {
return views;
}
public long getDuration() {
return duration;
}
public boolean isUploaderVerified() {
return uploaderVerified;
}
}

View File

@ -21,6 +21,10 @@ curl ${CURLOPTS[@]} $HOST/user/Kurzgesagt || exit 1
CHANNEL_NEXTPAGE=$(curl -s -o - -f $HOST/channel/UCsXVk37bltHxD1rDPwtNM8Q | jq -r .nextpage)
curl ${CURLOPTS[@]} $HOST/nextpage/channel/UCsXVk37bltHxD1rDPwtNM8Q -G --data-urlencode "nextpage=$CHANNEL_NEXTPAGE" || exit 1
# Channel Tab
CHANNEL_TAB_DATA=$(curl -s -o - -f $HOST/channel/UCsXVk37bltHxD1rDPwtNM8Q | jq -r .tabs[0].data)
curl ${CURLOPTS[@]} $HOST/channels/tabs -G --data-urlencode "data=$CHANNEL_TAB_DATA" || exit 1
# Playlist
curl ${CURLOPTS[@]} $HOST/playlists/PLQSoWXSpjA3-egtFq45DcUydZ885W7MTT || exit 1