mirror of
https://github.com/TeamNewPipe/NewPipeExtractor.git
synced 2025-04-29 00:10:35 +05:30
Merge pull request #1094 from AudricV/yt_support-more-channel-headers
[YouTube] Support more channel headers
This commit is contained in:
commit
93a90b816d
@ -219,6 +219,50 @@ public final class YoutubeChannelHelper {
|
|||||||
*/
|
*/
|
||||||
public static final class ChannelHeader {
|
public static final class ChannelHeader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of supported YouTube channel headers.
|
||||||
|
*/
|
||||||
|
public enum HeaderType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code c4TabbedHeaderRenderer} channel header type.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This header is returned on the majority of channels and contains the channel's name,
|
||||||
|
* its banner and its avatar and its subscriber count in most cases.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
C4_TABBED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@code interactiveTabbedHeaderRenderer} channel header type.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This header is returned for gaming topic channels, and only contains the channel's
|
||||||
|
* name, its banner and a poster as its "avatar".
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
INTERACTIVE_TABBED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code carouselHeaderRenderer} channel header type.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This header returns only the channel's name, its avatar and its subscriber count.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
CAROUSEL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code pageHeaderRenderer} channel header type.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This header returns only the channel's name and its avatar.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
PAGE
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The channel header JSON response.
|
* The channel header JSON response.
|
||||||
*/
|
*/
|
||||||
@ -226,17 +270,17 @@ public final class YoutubeChannelHelper {
|
|||||||
public final JsonObject json;
|
public final JsonObject json;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the header is a {@code carouselHeaderRenderer}.
|
* The type of the channel header.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* See the class documentation for more details.
|
* See the documentation of the {@link HeaderType} class for more details.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public final boolean isCarouselHeader;
|
public final HeaderType headerType;
|
||||||
|
|
||||||
private ChannelHeader(@Nonnull final JsonObject json, final boolean isCarouselHeader) {
|
private ChannelHeader(@Nonnull final JsonObject json, final HeaderType headerType) {
|
||||||
this.json = json;
|
this.json = json;
|
||||||
this.isCarouselHeader = isCarouselHeader;
|
this.headerType = headerType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +298,7 @@ public final class YoutubeChannelHelper {
|
|||||||
|
|
||||||
if (header.has("c4TabbedHeaderRenderer")) {
|
if (header.has("c4TabbedHeaderRenderer")) {
|
||||||
return Optional.of(header.getObject("c4TabbedHeaderRenderer"))
|
return Optional.of(header.getObject("c4TabbedHeaderRenderer"))
|
||||||
.map(json -> new ChannelHeader(json, false));
|
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.C4_TABBED));
|
||||||
} else if (header.has("carouselHeaderRenderer")) {
|
} else if (header.has("carouselHeaderRenderer")) {
|
||||||
return header.getObject("carouselHeaderRenderer")
|
return header.getObject("carouselHeaderRenderer")
|
||||||
.getArray("contents")
|
.getArray("contents")
|
||||||
@ -264,7 +308,14 @@ public final class YoutubeChannelHelper {
|
|||||||
.filter(item -> item.has("topicChannelDetailsRenderer"))
|
.filter(item -> item.has("topicChannelDetailsRenderer"))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(item -> item.getObject("topicChannelDetailsRenderer"))
|
.map(item -> item.getObject("topicChannelDetailsRenderer"))
|
||||||
.map(json -> new ChannelHeader(json, true));
|
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.CAROUSEL));
|
||||||
|
} else if (header.has("pageHeaderRenderer")) {
|
||||||
|
return Optional.of(header.getObject("pageHeaderRenderer"))
|
||||||
|
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.PAGE));
|
||||||
|
} else if (header.has("interactiveTabbedHeaderRenderer")) {
|
||||||
|
return Optional.of(header.getObject("interactiveTabbedHeaderRenderer"))
|
||||||
|
.map(json -> new ChannelHeader(json,
|
||||||
|
ChannelHeader.HeaderType.INTERACTIVE_TABBED));
|
||||||
} else {
|
} else {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper.ChannelHeader;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper.ChannelHeader.HeaderType;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeChannelTabExtractor.VideosTabExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeChannelTabExtractor.VideosTabExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
|
||||||
@ -59,7 +61,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
private JsonObject jsonResponse;
|
private JsonObject jsonResponse;
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
private Optional<YoutubeChannelHelper.ChannelHeader> channelHeader;
|
private Optional<ChannelHeader> channelHeader;
|
||||||
|
|
||||||
private String channelId;
|
private String channelId;
|
||||||
|
|
||||||
@ -116,11 +118,6 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
private Optional<JsonObject> getChannelHeaderJson() {
|
|
||||||
return channelHeader.map(it -> it.json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getUrl() throws ParsingException {
|
public String getUrl() throws ParsingException {
|
||||||
@ -134,7 +131,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getId() throws ParsingException {
|
public String getId() throws ParsingException {
|
||||||
return getChannelHeaderJson()
|
assertPageFetched();
|
||||||
|
return channelHeader.map(header -> header.json)
|
||||||
.flatMap(header -> Optional.ofNullable(header.getString("channelId"))
|
.flatMap(header -> Optional.ofNullable(header.getString("channelId"))
|
||||||
.or(() -> Optional.ofNullable(header.getObject("navigationEndpoint")
|
.or(() -> Optional.ofNullable(header.getObject("navigationEndpoint")
|
||||||
.getObject("browseEndpoint")
|
.getObject("browseEndpoint")
|
||||||
@ -147,8 +145,13 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
return channelAgeGateRenderer.getString("channelTitle");
|
final String title = channelAgeGateRenderer.getString("channelTitle");
|
||||||
|
if (isNullOrEmpty(title)) {
|
||||||
|
throw new ParsingException("Could not get channel name");
|
||||||
|
}
|
||||||
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String metadataRendererTitle = jsonResponse.getObject("metadata")
|
final String metadataRendererTitle = jsonResponse.getObject("metadata")
|
||||||
@ -158,53 +161,105 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
return metadataRendererTitle;
|
return metadataRendererTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getChannelHeaderJson().flatMap(header -> {
|
return channelHeader.map(header -> {
|
||||||
final Object title = header.get("title");
|
final JsonObject channelJson = header.json;
|
||||||
if (title instanceof String) {
|
switch (header.headerType) {
|
||||||
return Optional.of((String) title);
|
case PAGE:
|
||||||
} else if (title instanceof JsonObject) {
|
return channelJson.getObject("content")
|
||||||
final String headerName = getTextFromObject((JsonObject) title);
|
.getObject("pageHeaderViewModel")
|
||||||
if (!isNullOrEmpty(headerName)) {
|
.getObject("title")
|
||||||
return Optional.of(headerName);
|
.getObject("dynamicTextViewModel")
|
||||||
}
|
.getObject("text")
|
||||||
|
.getString("content", channelJson.getString("pageTitle"));
|
||||||
|
|
||||||
|
case CAROUSEL:
|
||||||
|
case INTERACTIVE_TABBED:
|
||||||
|
return getTextFromObject(channelJson.getObject("title"));
|
||||||
|
|
||||||
|
case C4_TABBED:
|
||||||
|
default:
|
||||||
|
return channelJson.getString("title");
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
})
|
||||||
}).orElseThrow(() -> new ParsingException("Could not get channel name"));
|
// The channel name from a microformatDataRenderer may be different from the one displayed,
|
||||||
|
// especially for auto-generated channels, depending on the language requested for the
|
||||||
|
// interface (hl parameter of InnerTube requests' payload)
|
||||||
|
.or(() -> Optional.ofNullable(jsonResponse.getObject("microformat")
|
||||||
|
.getObject("microformatDataRenderer")
|
||||||
|
.getString("title")))
|
||||||
|
.orElseThrow(() -> new ParsingException("Could not get channel name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAvatarUrl() throws ParsingException {
|
public String getAvatarUrl() throws ParsingException {
|
||||||
final JsonObject avatarJsonObjectContainer;
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
avatarJsonObjectContainer = channelAgeGateRenderer;
|
return Optional.ofNullable(channelAgeGateRenderer.getObject("avatar")
|
||||||
} else {
|
.getArray("thumbnails")
|
||||||
avatarJsonObjectContainer = getChannelHeaderJson()
|
.getObject(0)
|
||||||
|
.getString("url"))
|
||||||
|
.map(YoutubeParsingHelper::fixThumbnailUrl)
|
||||||
.orElseThrow(() -> new ParsingException("Could not get avatar URL"));
|
.orElseThrow(() -> new ParsingException("Could not get avatar URL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return YoutubeParsingHelper.fixThumbnailUrl(avatarJsonObjectContainer.getObject("avatar")
|
return channelHeader.map(header -> {
|
||||||
.getArray("thumbnails")
|
switch (header.headerType) {
|
||||||
.getObject(0)
|
case PAGE:
|
||||||
.getString("url"));
|
return header.json.getObject("content")
|
||||||
|
.getObject("pageHeaderViewModel")
|
||||||
|
.getObject("image")
|
||||||
|
.getObject("contentPreviewImageViewModel")
|
||||||
|
.getObject("image")
|
||||||
|
.getArray("sources")
|
||||||
|
.getObject(0)
|
||||||
|
.getString("url");
|
||||||
|
|
||||||
|
case INTERACTIVE_TABBED:
|
||||||
|
return header.json.getObject("boxArt")
|
||||||
|
.getArray("thumbnails")
|
||||||
|
.getObject(0)
|
||||||
|
.getString("url");
|
||||||
|
|
||||||
|
case C4_TABBED:
|
||||||
|
case CAROUSEL:
|
||||||
|
default:
|
||||||
|
return header.json.getObject("avatar")
|
||||||
|
.getArray("thumbnails")
|
||||||
|
.getObject(0)
|
||||||
|
.getString("url");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(YoutubeParsingHelper::fixThumbnailUrl)
|
||||||
|
.orElseThrow(() -> new ParsingException("Could not get avatar URL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBannerUrl() throws ParsingException {
|
public String getBannerUrl() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getChannelHeaderJson().flatMap(header -> Optional.ofNullable(
|
if (channelHeader.isPresent()) {
|
||||||
header.getObject("banner")
|
final ChannelHeader header = channelHeader.get();
|
||||||
.getArray("thumbnails")
|
if (header.headerType == HeaderType.PAGE) {
|
||||||
.getObject(0)
|
// No banner is available on pageHeaderRenderer headers
|
||||||
.getString("url")))
|
return null;
|
||||||
.filter(url -> !url.contains("s.ytimg.com") && !url.contains("default_banner"))
|
}
|
||||||
.map(YoutubeParsingHelper::fixThumbnailUrl)
|
|
||||||
// Channels may not have a banner, so no exception should be thrown if no banner is
|
return Optional.ofNullable(header.json.getObject("banner")
|
||||||
// found
|
.getArray("thumbnails")
|
||||||
// Return null in this case
|
.getObject(0)
|
||||||
.orElse(null);
|
.getString("url"))
|
||||||
|
.filter(url -> !url.contains("s.ytimg.com") && !url.contains("default_banner"))
|
||||||
|
.map(YoutubeParsingHelper::fixThumbnailUrl)
|
||||||
|
// Channels may not have a banner, so no exception should be thrown if no
|
||||||
|
// banner is found
|
||||||
|
// Return null in this case
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -214,25 +269,34 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
try {
|
try {
|
||||||
return YoutubeParsingHelper.getFeedUrlFrom(getId());
|
return YoutubeParsingHelper.getFeedUrlFrom(getId());
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get feed url", e);
|
throw new ParsingException("Could not get feed URL", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSubscriberCount() throws ParsingException {
|
public long getSubscriberCount() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
return UNKNOWN_SUBSCRIBER_COUNT;
|
return UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Optional<JsonObject> headerOpt = getChannelHeaderJson();
|
if (channelHeader.isPresent()) {
|
||||||
if (headerOpt.isPresent()) {
|
final ChannelHeader header = channelHeader.get();
|
||||||
final JsonObject header = headerOpt.get();
|
|
||||||
|
if (header.headerType == HeaderType.INTERACTIVE_TABBED
|
||||||
|
|| header.headerType == HeaderType.PAGE) {
|
||||||
|
// No subscriber count is available on interactiveTabbedHeaderRenderer and
|
||||||
|
// pageHeaderRenderer headers
|
||||||
|
return UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject headerJson = header.json;
|
||||||
JsonObject textObject = null;
|
JsonObject textObject = null;
|
||||||
|
|
||||||
if (header.has("subscriberCountText")) {
|
if (headerJson.has("subscriberCountText")) {
|
||||||
textObject = header.getObject("subscriberCountText");
|
textObject = headerJson.getObject("subscriberCountText");
|
||||||
} else if (header.has("subtitle")) {
|
} else if (headerJson.has("subtitle")) {
|
||||||
textObject = header.getObject("subtitle");
|
textObject = headerJson.getObject("subtitle");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textObject != null) {
|
if (textObject != null) {
|
||||||
@ -249,11 +313,34 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDescription() throws ParsingException {
|
public String getDescription() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (channelHeader.isPresent()) {
|
||||||
|
final ChannelHeader header = channelHeader.get();
|
||||||
|
|
||||||
|
if (header.headerType == HeaderType.PAGE) {
|
||||||
|
// A pageHeaderRenderer doesn't contain a description
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
|
||||||
|
/*
|
||||||
|
In an interactiveTabbedHeaderRenderer, the real description, is only available
|
||||||
|
in its header
|
||||||
|
The other one returned in non-About tabs accessible in the
|
||||||
|
microformatDataRenderer object of the response may be completely different
|
||||||
|
The description extracted is incomplete and the original one can be only
|
||||||
|
accessed from the About tab
|
||||||
|
*/
|
||||||
|
return getTextFromObject(header.json.getObject("description"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The description is cut and the original one can be only accessed from the About tab
|
||||||
return jsonResponse.getObject("metadata")
|
return jsonResponse.getObject("metadata")
|
||||||
.getObject("channelMetadataRenderer")
|
.getObject("channelMetadataRenderer")
|
||||||
.getString("description");
|
.getString("description");
|
||||||
@ -279,27 +366,39 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isVerified() throws ParsingException {
|
public boolean isVerified() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelHeader.isPresent()) {
|
if (channelHeader.isPresent()) {
|
||||||
final YoutubeChannelHelper.ChannelHeader header = channelHeader.get();
|
final ChannelHeader header = channelHeader.get();
|
||||||
|
|
||||||
// The CarouselHeaderRenderer does not contain any verification badges.
|
// carouselHeaderRenderer and pageHeaderRenderer does not contain any verification
|
||||||
// Since it is only shown on YT-internal channels or on channels of large organizations
|
// badges
|
||||||
// broadcasting live events, we can assume the channel to be verified.
|
// Since they are only shown on YouTube internal channels or on channels of large
|
||||||
if (header.isCarouselHeader) {
|
// organizations broadcasting live events, we can assume the channel to be verified
|
||||||
|
if (header.headerType == HeaderType.CAROUSEL || header.headerType == HeaderType.PAGE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
|
||||||
|
// If the header has an autoGenerated property, it should mean that the channel has
|
||||||
|
// been auto generated by YouTube: we can assume the channel to be verified in this
|
||||||
|
// case
|
||||||
|
return header.json.has("autoGenerated");
|
||||||
|
}
|
||||||
|
|
||||||
return YoutubeParsingHelper.isVerified(header.json.getArray("badges"));
|
return YoutubeParsingHelper.isVerified(header.json.getArray("badges"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public List<ListLinkHandler> getTabs() throws ParsingException {
|
public List<ListLinkHandler> getTabs() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer == null) {
|
if (channelAgeGateRenderer == null) {
|
||||||
return getTabsForNonAgeRestrictedChannels();
|
return getTabsForNonAgeRestrictedChannels();
|
||||||
}
|
}
|
||||||
@ -401,6 +500,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() throws ParsingException {
|
public List<String> getTags() throws ParsingException {
|
||||||
|
assertPageFetched();
|
||||||
if (channelAgeGateRenderer != null) {
|
if (channelAgeGateRenderer != null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertContains;
|
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertContains;
|
||||||
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl;
|
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank;
|
||||||
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertTabsContain;
|
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertTabsContain;
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestGetPageInNewExtractor;
|
import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestGetPageInNewExtractor;
|
||||||
@ -710,8 +711,10 @@ public class YoutubeChannelExtractorTest {
|
|||||||
// ChannelExtractor
|
// ChannelExtractor
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Test
|
||||||
@Override
|
@Override
|
||||||
public void testDescription() throws ParsingException {
|
public void testDescription() throws ParsingException {
|
||||||
|
assertNotBlank(extractor.getDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -885,4 +888,109 @@ public class YoutubeChannelExtractorTest {
|
|||||||
assertTrue(extractor.getTags().isEmpty());
|
assertTrue(extractor.getTags().isEmpty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class InteractiveTabbedHeader implements BaseChannelExtractorTest {
|
||||||
|
|
||||||
|
private static ChannelExtractor extractor;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() throws Exception {
|
||||||
|
YoutubeTestsUtils.ensureStateless();
|
||||||
|
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "interactiveTabbedHeader"));
|
||||||
|
extractor = YouTube.getChannelExtractor(
|
||||||
|
"https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg");
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testDescription() throws Exception {
|
||||||
|
final String description = extractor.getDescription();
|
||||||
|
assertContains("Minecraft", description);
|
||||||
|
assertContains("game", description);
|
||||||
|
assertContains("Mojang", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testAvatarUrl() throws Exception {
|
||||||
|
final String avatarUrl = extractor.getAvatarUrl();
|
||||||
|
assertIsSecureUrl(avatarUrl);
|
||||||
|
assertContains("yt3", avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testBannerUrl() throws Exception {
|
||||||
|
final String bannerUrl = extractor.getBannerUrl();
|
||||||
|
assertIsSecureUrl(bannerUrl);
|
||||||
|
assertContains("yt3", bannerUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testFeedUrl() throws Exception {
|
||||||
|
assertEquals(
|
||||||
|
"https://www.youtube.com/feeds/videos.xml?channel_id=UCQvWX73GQygcwXOTSf_VDVg",
|
||||||
|
extractor.getFeedUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testSubscriberCount() throws Exception {
|
||||||
|
// Subscriber count is not available on channels with an interactiveTabbedHeaderRenderer
|
||||||
|
assertEquals(ChannelExtractor.UNKNOWN_SUBSCRIBER_COUNT, extractor.getSubscriberCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testVerified() throws Exception {
|
||||||
|
assertTrue(extractor.isVerified());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testTabs() throws Exception {
|
||||||
|
// Gaming topic channels tabs are not yet supported, so an empty list should be returned
|
||||||
|
assertTrue(extractor.getTabs().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testTags() throws Exception {
|
||||||
|
assertTrue(extractor.getTags().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testServiceId() throws Exception {
|
||||||
|
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testName() throws Exception {
|
||||||
|
assertContains("Minecraft", extractor.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testId() throws Exception {
|
||||||
|
assertEquals("UCQvWX73GQygcwXOTSf_VDVg", extractor.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testUrl() throws Exception {
|
||||||
|
assertEquals("https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg",
|
||||||
|
extractor.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void testOriginalUrl() throws Exception {
|
||||||
|
assertEquals("https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg",
|
||||||
|
extractor.getOriginalUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"httpMethod": "GET",
|
||||||
|
"url": "https://www.youtube.com/sw.js",
|
||||||
|
"headers": {
|
||||||
|
"Referer": [
|
||||||
|
"https://www.youtube.com"
|
||||||
|
],
|
||||||
|
"Origin": [
|
||||||
|
"https://www.youtube.com"
|
||||||
|
],
|
||||||
|
"Accept-Language": [
|
||||||
|
"en-GB, en;q\u003d0.9"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"languageCode": "en",
|
||||||
|
"countryCode": "GB"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"responseCode": 200,
|
||||||
|
"responseMessage": "",
|
||||||
|
"responseHeaders": {
|
||||||
|
"access-control-allow-credentials": [
|
||||||
|
"true"
|
||||||
|
],
|
||||||
|
"access-control-allow-origin": [
|
||||||
|
"https://www.youtube.com"
|
||||||
|
],
|
||||||
|
"alt-svc": [
|
||||||
|
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
|
||||||
|
],
|
||||||
|
"cache-control": [
|
||||||
|
"private, max-age\u003d0"
|
||||||
|
],
|
||||||
|
"content-type": [
|
||||||
|
"text/javascript; charset\u003dutf-8"
|
||||||
|
],
|
||||||
|
"cross-origin-opener-policy-report-only": [
|
||||||
|
"same-origin; report-to\u003d\"youtube_main\""
|
||||||
|
],
|
||||||
|
"date": [
|
||||||
|
"Tue, 08 Aug 2023 17:04:33 GMT"
|
||||||
|
],
|
||||||
|
"expires": [
|
||||||
|
"Tue, 08 Aug 2023 17:04:33 GMT"
|
||||||
|
],
|
||||||
|
"origin-trial": [
|
||||||
|
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
|
||||||
|
],
|
||||||
|
"p3p": [
|
||||||
|
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||||
|
],
|
||||||
|
"permissions-policy": [
|
||||||
|
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||||
|
],
|
||||||
|
"report-to": [
|
||||||
|
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
|
||||||
|
],
|
||||||
|
"server": [
|
||||||
|
"ESF"
|
||||||
|
],
|
||||||
|
"set-cookie": [
|
||||||
|
"YSC\u003dV1ZtomDP-24; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
|
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 11-Nov-2020 17:04:33 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
|
"CONSENT\u003dPENDING+547; expires\u003dThu, 07-Aug-2025 17:04:33 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||||
|
],
|
||||||
|
"strict-transport-security": [
|
||||||
|
"max-age\u003d31536000"
|
||||||
|
],
|
||||||
|
"x-content-type-options": [
|
||||||
|
"nosniff"
|
||||||
|
],
|
||||||
|
"x-frame-options": [
|
||||||
|
"SAMEORIGIN"
|
||||||
|
],
|
||||||
|
"x-xss-protection": [
|
||||||
|
"0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"responseBody": "\n self.addEventListener(\u0027install\u0027, event \u003d\u003e {\n event.waitUntil(self.skipWaiting());\n });\n self.addEventListener(\u0027activate\u0027, event \u003d\u003e {\n event.waitUntil(\n self.clients.claim().then(() \u003d\u003e self.registration.unregister()));\n });\n ",
|
||||||
|
"latestUrl": "https://www.youtube.com/sw.js"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user