2022-10-22 15:28:48 +02:00
|
|
|
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
|
|
|
|
|
|
|
import com.grack.nanojson.JsonArray;
|
|
|
|
import com.grack.nanojson.JsonObject;
|
|
|
|
import com.grack.nanojson.JsonWriter;
|
|
|
|
import org.schabi.newpipe.extractor.InfoItem;
|
|
|
|
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
|
|
|
|
import org.schabi.newpipe.extractor.Page;
|
|
|
|
import org.schabi.newpipe.extractor.StreamingService;
|
|
|
|
import org.schabi.newpipe.extractor.channel.ChannelTabExtractor;
|
|
|
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
|
|
|
import org.schabi.newpipe.extractor.downloader.Response;
|
|
|
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
|
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|
|
|
import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler;
|
|
|
|
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
|
|
|
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
|
|
|
|
|
|
|
import javax.annotation.Nonnull;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Map;
|
|
|
|
import java.util.function.Consumer;
|
|
|
|
|
2022-10-23 23:09:40 +02:00
|
|
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
2022-10-22 15:28:48 +02:00
|
|
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
|
|
|
|
|
|
public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
|
|
|
|
private JsonObject initialData;
|
|
|
|
private JsonObject tabData;
|
|
|
|
|
|
|
|
private String redirectedChannelId;
|
|
|
|
|
|
|
|
public YoutubeChannelTabExtractor(final StreamingService service,
|
|
|
|
final ChannelTabHandler linkHandler) {
|
|
|
|
super(service, linkHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nullable
|
|
|
|
String getParams() {
|
|
|
|
switch (getTab()) {
|
|
|
|
case Playlists:
|
|
|
|
return "EglwbGF5bGlzdHMgAQ%3D%3D";
|
|
|
|
case Livestreams:
|
|
|
|
return "EgdzdHJlYW1z8gYECgJ6AA%3D%3D";
|
|
|
|
case Shorts:
|
|
|
|
return "EgZzaG9ydHPyBgUKA5oBAA%3D%3D";
|
|
|
|
case Channels:
|
|
|
|
return "EghjaGFubmVsc_IGBAoCUgA%3D";
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
String getUrlSuffix() {
|
|
|
|
switch (getTab()) {
|
|
|
|
case Playlists:
|
|
|
|
return "/playlists";
|
|
|
|
case Livestreams:
|
|
|
|
return "/streams";
|
|
|
|
case Shorts:
|
|
|
|
return "/shorts";
|
|
|
|
case Channels:
|
|
|
|
return "/channels";
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
|
|
|
ExtractionException {
|
|
|
|
final String params = getParams();
|
|
|
|
if (params == null) {
|
2022-10-23 23:09:40 +02:00
|
|
|
throw new ExtractionException("tab " + getLinkHandler().getTab().name() + " not supported");
|
2022-10-22 15:28:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
final String id = resolveChannelId(super.getId());
|
|
|
|
final ChannelResponseData data = getChannelResponse(id, params,
|
|
|
|
getExtractorLocalization(), getExtractorContentCountry());
|
|
|
|
|
|
|
|
initialData = data.responseJson;
|
|
|
|
redirectedChannelId = data.channelId;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public String getUrl() throws ParsingException {
|
|
|
|
try {
|
|
|
|
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl(
|
|
|
|
"channel/" + getId() + getUrlSuffix());
|
|
|
|
} catch (final ParsingException e) {
|
|
|
|
return super.getUrl();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public String getId() throws ParsingException {
|
|
|
|
final String channelId = initialData.getObject("header")
|
|
|
|
.getObject("c4TabbedHeaderRenderer")
|
|
|
|
.getString("channelId", "");
|
|
|
|
|
|
|
|
if (!channelId.isEmpty()) {
|
|
|
|
return channelId;
|
|
|
|
} else if (!isNullOrEmpty(redirectedChannelId)) {
|
|
|
|
return redirectedChannelId;
|
|
|
|
} else {
|
|
|
|
throw new ParsingException("Could not get channel id");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-23 10:27:35 +02:00
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public String getName() throws ParsingException {
|
|
|
|
try {
|
|
|
|
return initialData.getObject("header").getObject("c4TabbedHeaderRenderer")
|
|
|
|
.getString("title");
|
|
|
|
} catch (final Exception e) {
|
|
|
|
throw new ParsingException("Could not get channel name", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-22 15:28:48 +02:00
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
|
|
|
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
|
|
|
|
|
|
|
Page nextPage = null;
|
|
|
|
|
|
|
|
if (getTabData() != null) {
|
|
|
|
final JsonObject tabContent = tabData.getObject("content");
|
|
|
|
JsonArray items = tabContent
|
|
|
|
.getObject("sectionListRenderer")
|
|
|
|
.getArray("contents").getObject(0).getObject("itemSectionRenderer")
|
|
|
|
.getArray("contents").getObject(0).getObject("gridRenderer").getArray("items");
|
|
|
|
|
|
|
|
if (items.isEmpty()) {
|
|
|
|
items = tabContent.getObject("richGridRenderer").getArray("contents");
|
2022-10-23 10:27:35 +02:00
|
|
|
|
|
|
|
if (items.isEmpty()) {
|
|
|
|
items = tabContent.getObject("sectionListRenderer").getArray("contents");
|
|
|
|
}
|
2022-10-22 15:28:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
final List<String> channelIds = new ArrayList<>();
|
|
|
|
channelIds.add(getName());
|
|
|
|
channelIds.add(getUrl());
|
|
|
|
final JsonObject continuation = collectItemsFrom(collector, items, channelIds);
|
|
|
|
|
|
|
|
nextPage = getNextPageFrom(continuation, channelIds);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new InfoItemsPage<>(collector, nextPage);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public InfoItemsPage<InfoItem> getPage(final Page page)
|
|
|
|
throws IOException, ExtractionException {
|
|
|
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
|
|
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
|
|
|
}
|
|
|
|
|
|
|
|
final List<String> channelIds = page.getIds();
|
|
|
|
|
|
|
|
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
|
|
|
final Map<String, List<String>> headers = new HashMap<>();
|
|
|
|
addClientInfoHeaders(headers);
|
|
|
|
|
|
|
|
final Response response = getDownloader().post(page.getUrl(), headers, page.getBody(),
|
|
|
|
getExtractorLocalization());
|
|
|
|
|
|
|
|
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
|
|
|
|
|
|
|
final JsonObject sectionListContinuation = ajaxJson.getArray("onResponseReceivedActions")
|
|
|
|
.getObject(0)
|
|
|
|
.getObject("appendContinuationItemsAction");
|
|
|
|
|
|
|
|
final JsonObject continuation = collectItemsFrom(collector, sectionListContinuation
|
|
|
|
.getArray("continuationItems"), channelIds);
|
|
|
|
|
|
|
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuation, channelIds));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nullable
|
|
|
|
private JsonObject getTabData() {
|
|
|
|
if (this.tabData != null) {
|
|
|
|
return this.tabData;
|
|
|
|
}
|
|
|
|
|
|
|
|
final JsonArray tabs = initialData.getObject("contents")
|
|
|
|
.getObject("twoColumnBrowseResultsRenderer")
|
|
|
|
.getArray("tabs");
|
|
|
|
|
|
|
|
JsonObject foundTab = null;
|
|
|
|
for (final Object tab : tabs) {
|
|
|
|
if (((JsonObject) tab).has("tabRenderer")) {
|
|
|
|
if (((JsonObject) tab).getObject("tabRenderer").getObject("endpoint")
|
|
|
|
.getObject("commandMetadata").getObject("webCommandMetadata")
|
|
|
|
.getString("url").endsWith(getUrlSuffix())) {
|
|
|
|
foundTab = ((JsonObject) tab).getObject("tabRenderer");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No tab
|
|
|
|
if (foundTab == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No content
|
|
|
|
final JsonArray tabContents = foundTab.getObject("content").getObject("sectionListRenderer")
|
|
|
|
.getArray("contents").getObject(0)
|
|
|
|
.getObject("itemSectionRenderer").getArray("contents");
|
|
|
|
if (tabContents.size() == 1 && tabContents.getObject(0).has("messageRenderer")) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tabData = foundTab;
|
|
|
|
return foundTab;
|
|
|
|
}
|
|
|
|
|
2022-10-23 10:27:35 +02:00
|
|
|
@Nullable
|
2022-10-22 15:28:48 +02:00
|
|
|
private JsonObject collectItemsFrom(@Nonnull final MultiInfoItemsCollector collector,
|
2022-10-23 10:27:35 +02:00
|
|
|
@Nonnull final JsonArray items,
|
|
|
|
@Nonnull final List<String> channelIds) {
|
2022-10-22 15:28:48 +02:00
|
|
|
JsonObject continuation = null;
|
|
|
|
|
|
|
|
for (final Object object : items) {
|
|
|
|
final JsonObject item = (JsonObject) object;
|
2022-10-23 10:27:35 +02:00
|
|
|
final JsonObject optContinuation = collectItem(
|
|
|
|
collector, item, channelIds);
|
|
|
|
if (optContinuation != null) {
|
|
|
|
continuation = optContinuation;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return continuation;
|
|
|
|
}
|
2022-10-22 15:28:48 +02:00
|
|
|
|
2022-10-23 10:27:35 +02:00
|
|
|
@Nullable
|
|
|
|
private JsonObject collectItem(@Nonnull final MultiInfoItemsCollector collector,
|
|
|
|
@Nonnull final JsonObject item,
|
|
|
|
@Nonnull final List<String> channelIds) {
|
|
|
|
final Consumer<JsonObject> commitVideo = videoRenderer -> collector.commit(
|
|
|
|
new YoutubeStreamInfoItemExtractor(videoRenderer, getTimeAgoParser()) {
|
2022-10-22 15:28:48 +02:00
|
|
|
@Override
|
|
|
|
public String getUploaderName() {
|
2022-10-23 10:27:35 +02:00
|
|
|
return channelIds.get(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getUploaderUrl() {
|
|
|
|
return channelIds.get(1);
|
2022-10-22 15:28:48 +02:00
|
|
|
}
|
|
|
|
});
|
2022-10-23 10:27:35 +02:00
|
|
|
|
|
|
|
if (item.has("gridVideoRenderer")) {
|
|
|
|
commitVideo.accept(item.getObject("gridVideoRenderer"));
|
|
|
|
} else if (item.has("richItemRenderer")) {
|
|
|
|
final JsonObject richItem = item.getObject("richItemRenderer").getObject("content");
|
|
|
|
|
|
|
|
if (richItem.has("videoRenderer")) {
|
|
|
|
commitVideo.accept(richItem.getObject("videoRenderer"));
|
|
|
|
|
|
|
|
} else if (richItem.has("reelItemRenderer")) {
|
|
|
|
commitVideo.accept(richItem.getObject("reelItemRenderer"));
|
2022-10-22 15:28:48 +02:00
|
|
|
}
|
2022-10-23 10:27:35 +02:00
|
|
|
} else if (item.has("gridPlaylistRenderer")) {
|
|
|
|
collector.commit(new YoutubePlaylistInfoItemExtractor(
|
|
|
|
item.getObject("gridPlaylistRenderer")) {
|
|
|
|
@Override
|
|
|
|
public String getUploaderName() {
|
|
|
|
return channelIds.get(0);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (item.has("gridChannelRenderer")) {
|
|
|
|
collector.commit(new YoutubeChannelInfoItemExtractor(
|
|
|
|
item.getObject("gridChannelRenderer")));
|
|
|
|
} else if (item.has("shelfRenderer")) {
|
|
|
|
return collectItem(collector, item.getObject("shelfRenderer")
|
|
|
|
.getObject("content"), channelIds);
|
|
|
|
} else if (item.has("itemSectionRenderer")) {
|
|
|
|
return collectItemsFrom(collector, item.getObject("itemSectionRenderer")
|
|
|
|
.getArray("contents"), channelIds);
|
|
|
|
} else if (item.has("horizontalListRenderer")) {
|
|
|
|
return collectItemsFrom(collector, item.getObject("horizontalListRenderer")
|
|
|
|
.getArray("items"), channelIds);
|
|
|
|
} else if (item.has("expandedShelfContentsRenderer")) {
|
|
|
|
return collectItemsFrom(collector, item.getObject("expandedShelfContentsRenderer")
|
|
|
|
.getArray("items"), channelIds);
|
|
|
|
} else if (item.has("continuationItemRenderer")) {
|
|
|
|
return item.getObject("continuationItemRenderer");
|
2022-10-22 15:28:48 +02:00
|
|
|
}
|
2022-10-23 10:27:35 +02:00
|
|
|
return null;
|
2022-10-22 15:28:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Nullable
|
|
|
|
private Page getNextPageFrom(final JsonObject continuations,
|
|
|
|
final List<String> channelIds) throws IOException,
|
|
|
|
ExtractionException {
|
|
|
|
if (isNullOrEmpty(continuations)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
final JsonObject continuationEndpoint = continuations.getObject("continuationEndpoint");
|
|
|
|
final String continuation = continuationEndpoint.getObject("continuationCommand")
|
|
|
|
.getString("token");
|
|
|
|
|
|
|
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(getExtractorLocalization(),
|
|
|
|
getExtractorContentCountry())
|
|
|
|
.value("continuation", continuation)
|
|
|
|
.done())
|
|
|
|
.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
|
|
|
return new Page(YOUTUBEI_V1_URL + "browse?key=" + getKey()
|
|
|
|
+ DISABLE_PRETTY_PRINT_PARAMETER, null, channelIds, null, body);
|
|
|
|
}
|
|
|
|
}
|