2
0
mirror of https://github.com/TeamNewPipe/NewPipeExtractor synced 2025-08-22 09:57:38 +00:00

Merge pull request #1354 from AudricV/yt_more_kiosks_and_trending_deprecation

This commit is contained in:
Stypox 2025-07-31 23:24:13 +02:00 committed by GitHub
commit 0a7b72aec6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 4849 additions and 39 deletions

View File

@ -0,0 +1,20 @@
package org.schabi.newpipe.extractor.exceptions;
/**
* Exception for contents not supported in a country.
*
* <p>
* Unsupported content means content is not intentionally geographically restricted such as for
* distribution rights, for which {@link GeographicRestrictionException} should be used instead.
* </p>
*/
public class UnsupportedContentInCountryException extends ContentNotAvailableException {
public UnsupportedContentInCountryException(final String message) {
super(message);
}
public UnsupportedContentInCountryException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -45,6 +45,12 @@ final class ClientsConstants {
static final String WEB_EMBEDDED_CLIENT_NAME = "WEB_EMBEDDED_PLAYER";
static final String WEB_EMBEDDED_CLIENT_VERSION = "1.20250121.00.00";
// WEB_MUSIC_ANALYTICS (YouTube charts)
static final String WEB_MUSIC_ANALYTICS_CLIENT_ID = "31";
static final String WEB_MUSIC_ANALYTICS_CLIENT_NAME = "WEB_MUSIC_ANALYTICS";
static final String WEB_MUSIC_ANALYTICS_CLIENT_VERSION = "2.0";
// IOS (iOS YouTube app) client fields
static final String IOS_CLIENT_ID = "5";

View File

@ -1,5 +1,8 @@
package org.schabi.newpipe.extractor.services.youtube;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_ID;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_NAME;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_VERSION;
@ -16,11 +19,11 @@ import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_NAME;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_EMBEDDED_CLIENT_ID;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_EMBEDDED_CLIENT_NAME;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_EMBEDDED_CLIENT_VERSION;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_HARDCODED_CLIENT_VERSION;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_HARDCODED_CLIENT_VERSION;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_MUSIC_ANALYTICS_CLIENT_ID;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_MUSIC_ANALYTICS_CLIENT_NAME;
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_MUSIC_ANALYTICS_CLIENT_VERSION;
// TODO: add docs
@ -38,28 +41,28 @@ public final class InnertubeClientRequestInfo {
@Nonnull
public String clientVersion;
@Nonnull
public String clientScreen;
@Nullable
public String clientId;
@Nullable
public String clientScreen;
@Nullable
public String visitorData;
private ClientInfo(@Nonnull final String clientName,
@Nonnull final String clientVersion,
@Nonnull final String clientScreen,
@Nullable final String clientId,
@Nonnull final String clientId,
@Nullable final String clientScreen,
@Nullable final String visitorData) {
this.clientName = clientName;
this.clientVersion = clientVersion;
this.clientScreen = clientScreen;
this.clientId = clientId;
this.clientScreen = clientScreen;
this.visitorData = visitorData;
}
}
public static final class DeviceInfo {
@Nonnull
@Nullable
public String platform;
@Nullable
public String deviceMake;
@ -71,7 +74,7 @@ public final class InnertubeClientRequestInfo {
public String osVersion;
public int androidSdkVersion;
private DeviceInfo(@Nonnull final String platform,
private DeviceInfo(@Nullable final String platform,
@Nullable final String deviceMake,
@Nullable final String deviceModel,
@Nullable final String osName,
@ -96,8 +99,8 @@ public final class InnertubeClientRequestInfo {
public static InnertubeClientRequestInfo ofWebClient() {
return new InnertubeClientRequestInfo(
new InnertubeClientRequestInfo.ClientInfo(
WEB_CLIENT_NAME, WEB_HARDCODED_CLIENT_VERSION, WATCH_CLIENT_SCREEN,
WEB_CLIENT_ID, null),
WEB_CLIENT_NAME, WEB_HARDCODED_CLIENT_VERSION, WEB_CLIENT_ID,
WATCH_CLIENT_SCREEN, null),
new InnertubeClientRequestInfo.DeviceInfo(DESKTOP_CLIENT_PLATFORM, null, null,
null, null, -1));
}
@ -106,17 +109,27 @@ public final class InnertubeClientRequestInfo {
public static InnertubeClientRequestInfo ofWebEmbeddedPlayerClient() {
return new InnertubeClientRequestInfo(
new InnertubeClientRequestInfo.ClientInfo(WEB_EMBEDDED_CLIENT_NAME,
WEB_REMIX_HARDCODED_CLIENT_VERSION, EMBED_CLIENT_SCREEN,
WEB_EMBEDDED_CLIENT_ID, null),
WEB_EMBEDDED_CLIENT_VERSION, WEB_EMBEDDED_CLIENT_ID, EMBED_CLIENT_SCREEN,
null),
new InnertubeClientRequestInfo.DeviceInfo(DESKTOP_CLIENT_PLATFORM, null, null,
null, null, -1));
}
@Nonnull
public static InnertubeClientRequestInfo ofWebMusicAnalyticsChartsClient() {
return new InnertubeClientRequestInfo(
new InnertubeClientRequestInfo.ClientInfo(WEB_MUSIC_ANALYTICS_CLIENT_NAME,
WEB_MUSIC_ANALYTICS_CLIENT_VERSION, WEB_MUSIC_ANALYTICS_CLIENT_ID, null,
null),
new InnertubeClientRequestInfo.DeviceInfo(null, null, null,
null, null, -1));
}
@Nonnull
public static InnertubeClientRequestInfo ofAndroidClient() {
return new InnertubeClientRequestInfo(
new InnertubeClientRequestInfo.ClientInfo(ANDROID_CLIENT_NAME,
ANDROID_CLIENT_VERSION, WATCH_CLIENT_SCREEN, ANDROID_CLIENT_ID, null),
ANDROID_CLIENT_VERSION, ANDROID_CLIENT_ID, WATCH_CLIENT_SCREEN, null),
new InnertubeClientRequestInfo.DeviceInfo(MOBILE_CLIENT_PLATFORM, null, null,
"Android", "15", 35));
}
@ -125,7 +138,7 @@ public final class InnertubeClientRequestInfo {
public static InnertubeClientRequestInfo ofIosClient() {
return new InnertubeClientRequestInfo(
new InnertubeClientRequestInfo.ClientInfo(IOS_CLIENT_NAME, IOS_CLIENT_VERSION,
WATCH_CLIENT_SCREEN, IOS_CLIENT_ID, null),
IOS_CLIENT_ID, WATCH_CLIENT_SCREEN, null),
new InnertubeClientRequestInfo.DeviceInfo(MOBILE_CLIENT_PLATFORM, "Apple",
IOS_DEVICE_MODEL, "iOS", IOS_OS_VERSION, -1));
}

View File

@ -1181,8 +1181,8 @@ public final class YoutubeParsingHelper {
* @param name The X-YouTube-Client-Name value.
* @param version X-YouTube-Client-Version value.
*/
static Map<String, List<String>> getClientHeaders(@Nonnull final String name,
@Nonnull final String version) {
public static Map<String, List<String>> getClientHeaders(@Nonnull final String name,
@Nonnull final String version) {
return Map.of("X-YouTube-Client-Name", List.of(name),
"X-YouTube-Client-Version", List.of(version));
}
@ -1525,7 +1525,7 @@ public final class YoutubeParsingHelper {
}
@Nonnull
static JsonBuilder<JsonObject> prepareJsonBuilder(
public static JsonBuilder<JsonObject> prepareJsonBuilder(
@Nonnull final Localization localization,
@Nonnull final ContentCountry contentCountry,
@Nonnull final InnertubeClientRequestInfo innertubeClientRequestInfo,
@ -1534,9 +1534,15 @@ public final class YoutubeParsingHelper {
.object("context")
.object("client")
.value("clientName", innertubeClientRequestInfo.clientInfo.clientName)
.value("clientVersion", innertubeClientRequestInfo.clientInfo.clientVersion)
.value("clientScreen", innertubeClientRequestInfo.clientInfo.clientScreen)
.value("platform", innertubeClientRequestInfo.deviceInfo.platform);
.value("clientVersion", innertubeClientRequestInfo.clientInfo.clientVersion);
if (innertubeClientRequestInfo.clientInfo.clientScreen != null) {
builder.value("clientScreen", innertubeClientRequestInfo.clientInfo.clientScreen);
}
if (innertubeClientRequestInfo.deviceInfo.platform != null) {
builder.value("platform", innertubeClientRequestInfo.deviceInfo.platform);
}
if (innertubeClientRequestInfo.clientInfo.visitorData != null) {
builder.value("visitorData", innertubeClientRequestInfo.clientInfo.visitorData);

View File

@ -35,14 +35,24 @@ import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSearchExt
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSubscriptionExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSuggestionExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeTrendingExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeLiveExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingGamingVideosExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingMoviesAndShowsTrailersExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingMusicExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingPodcastsEpisodesExtractor;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelTabLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeCommentsLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeLiveLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubePlaylistLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeTrendingGamingVideosLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeTrendingLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeTrendingMusicLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeTrendingPodcastsEpisodesLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
@ -154,20 +164,71 @@ public class YoutubeService extends StreamingService {
@Override
public KioskList getKioskList() throws ExtractionException {
final KioskList list = new KioskList(this);
final ListLinkHandlerFactory h = YoutubeTrendingLinkHandlerFactory.getInstance();
final ListLinkHandlerFactory trendingLHF = YoutubeTrendingLinkHandlerFactory.INSTANCE;
final ListLinkHandlerFactory runningLivesLHF =
YoutubeLiveLinkHandlerFactory.INSTANCE;
final ListLinkHandlerFactory trendingPodcastsEpisodesLHF =
YoutubeTrendingPodcastsEpisodesLinkHandlerFactory.INSTANCE;
final ListLinkHandlerFactory trendingGamingVideosLHF =
YoutubeTrendingGamingVideosLinkHandlerFactory.INSTANCE;
final ListLinkHandlerFactory trendingMoviesAndShowsLHF =
YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory.INSTANCE;
final ListLinkHandlerFactory trendingMusicLHF =
YoutubeTrendingMusicLinkHandlerFactory.INSTANCE;
// add kiosks here e.g.:
try {
list.addKioskEntry(
(streamingService, url, id) -> new YoutubeLiveExtractor(
YoutubeService.this,
runningLivesLHF.fromUrl(url),
id),
runningLivesLHF,
YoutubeLiveLinkHandlerFactory.KIOSK_ID
);
list.addKioskEntry(
(streamingService, url, id) -> new YoutubeTrendingPodcastsEpisodesExtractor(
YoutubeService.this,
trendingPodcastsEpisodesLHF.fromUrl(url),
id),
trendingPodcastsEpisodesLHF,
YoutubeTrendingPodcastsEpisodesLinkHandlerFactory.KIOSK_ID
);
list.addKioskEntry(
(streamingService, url, id) -> new YoutubeTrendingGamingVideosExtractor(
YoutubeService.this,
trendingGamingVideosLHF.fromUrl(url),
id),
trendingGamingVideosLHF,
YoutubeTrendingGamingVideosLinkHandlerFactory.KIOSK_ID
);
list.addKioskEntry(
(streamingService, url, id) ->
new YoutubeTrendingMoviesAndShowsTrailersExtractor(
YoutubeService.this,
trendingMoviesAndShowsLHF.fromUrl(url),
id),
trendingMoviesAndShowsLHF,
YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory.KIOSK_ID
);
list.addKioskEntry(
(streamingService, url, id) -> new YoutubeTrendingMusicExtractor(
YoutubeService.this,
trendingMusicLHF.fromUrl(url),
id),
trendingMusicLHF,
YoutubeTrendingMusicLinkHandlerFactory.KIOSK_ID
);
// Deprecated (i.e. removed from the interface of YouTube) since July 21, 2025
list.addKioskEntry(
(streamingService, url, id) -> new YoutubeTrendingExtractor(
YoutubeService.this,
h.fromUrl(url),
trendingLHF.fromUrl(url),
id
),
h,
trendingLHF,
YoutubeTrendingExtractor.KIOSK_ID
);
list.setDefaultKiosk(YoutubeTrendingExtractor.KIOSK_ID);
list.setDefaultKiosk(YoutubeLiveLinkHandlerFactory.KIOSK_ID);
} catch (final Exception e) {
throw new ExtractionException(e);
}

View File

@ -0,0 +1,239 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import org.schabi.newpipe.extractor.stream.StreamType;
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.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientHeaders;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getOriginReferrerHeaders;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getThumbnailsFromInfoItem;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/**
* Base class parsing responses from YouTube Charts for all trending video charts.
*
* <p>
* Note: YouTube Charts isn't officially supported in all YouTube supported countries (there are
* fewer countries in the {@code LAUNCHED_CHART_COUNTRIES} array of YouTube Charts' HTML responses
* than in the YouTube country selector).
* </p>
*
* <p>
* For some trends, some videos are still returned in unsupported countries, even if there are
* fewer than in a supported country, for others an HTTP 400 error is returned saying
* {@code Request contains an invalid argument.}.
* </p>
*/
abstract class YoutubeChartsBaseKioskExtractor extends KioskExtractor<StreamInfoItem> {
// Extracted from YouTube Charts' HTML, in the array named LAUNCHED_CHART_COUNTRIES
protected static final Set<String> YT_CHARTS_SUPPORTED_COUNTRY_CODES = Set.of(
"AE", "AR", "AT", "AU", "BE", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CZ", "DE",
"DK", "DO", "EC", "EE", "EG", "ES", "FI", "FR", "GB", "GT", "HN", "HU", "ID", "IE",
"IL", "IN", "IS", "IT", "JP", "KE", "KR", "LU", "MX", "NG", "NI", "NL", "NO", "NZ",
"PA", "PE", "PL", "PT", "PY", "RO", "RS", "RU", "SA", "SE", "SV", "TR", "TZ", "UA",
"UG", "US", "UY", "ZA", "ZW");
protected static final String YT_CHARTS_ENDPOINT =
"https://charts.youtube.com/youtubei/v1/browse?alt=json&"
+ DISABLE_PRETTY_PRINT_PARAMETER;
protected final String chartType;
protected JsonObject browseResponse;
protected YoutubeChartsBaseKioskExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId,
final String chartType) {
super(streamingService, linkHandler, kioskId);
this.chartType = chartType;
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
final Localization localization = getExtractorLocalization();
final ContentCountry contentCountry = getExtractorContentCountry();
final InnertubeClientRequestInfo innertubeClientRequestInfo =
InnertubeClientRequestInfo.ofWebMusicAnalyticsChartsClient();
final byte[] body = JsonWriter.string(prepareJsonBuilder(getExtractorLocalization(),
contentCountry, innertubeClientRequestInfo, null)
.value("browseId", "FEmusic_analytics_charts_home")
.value("query", "perspective=CHART_DETAILS&chart_params_country_code="
+ contentCountry.getCountryCode() + "&chart_params_chart_type="
+ chartType)
.done())
.getBytes(StandardCharsets.UTF_8);
final var headers = new HashMap<>(getOriginReferrerHeaders("https://charts.youtube.com"));
headers.putAll(getClientHeaders(innertubeClientRequestInfo.clientInfo.clientId,
innertubeClientRequestInfo.clientInfo.clientVersion));
browseResponse = JsonUtils.toJsonObject(getValidJsonResponseBody(
getDownloader().postWithContentTypeJson(
YT_CHARTS_ENDPOINT, headers, body, localization)));
}
@Nonnull
@Override
public abstract String getName() throws ParsingException;
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
final JsonArray videos = browseResponse.getObject("contents")
.getObject("sectionListRenderer")
.getArray("contents")
.getObject(0)
.getObject("musicAnalyticsSectionRenderer")
.getObject("content")
.getArray("videos")
.getObject(0)
.getArray("videoViews");
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
videos.streamAsJsonObjects()
.forEachOrdered(video -> collector.commit(
new YoutubeChartsVideoInfoItemExtractor(video)));
return new InfoItemsPage<>(collector, null);
}
@Override
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
// There is no continuation in charts
return InfoItemsPage.emptyPage();
}
static final class YoutubeChartsVideoInfoItemExtractor
implements StreamInfoItemExtractor {
@Nonnull
private final JsonObject videoObject;
YoutubeChartsVideoInfoItemExtractor(@Nonnull final JsonObject videoObject) {
this.videoObject = videoObject;
}
@Override
public StreamType getStreamType() {
// There are only video streams in YouTube Charts, at least for now
return StreamType.VIDEO_STREAM;
}
@Override
public boolean isAd() {
return false;
}
@Override
public long getDuration() {
return videoObject.getInt("videoDuration", -1);
}
@Override
public long getViewCount() {
// View counts aren't returned, at least for now
return -1;
}
@Override
public String getUploaderName() {
return videoObject.getString("channelName");
}
@Override
public String getUploaderUrl() throws ParsingException {
final String channelId = videoObject.getString("externalChannelId");
if (isNullOrEmpty(channelId)) {
throw new ParsingException("Could not get channel ID");
}
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + channelId);
}
@Override
public boolean isUploaderVerified() {
// We don't have any info on this, at least for now
return false;
}
@Nullable
@Override
public String getTextualUploadDate() {
return null;
}
@Nonnull
@Override
public DateWrapper getUploadDate() {
final JsonObject releaseDate = videoObject.getObject("releaseDate");
return new DateWrapper(OffsetDateTime.of(
releaseDate.getInt("year"),
releaseDate.getInt("month"),
releaseDate.getInt("day"),
0,
0,
0,
0,
// We request that times should be returned with 0 offset to UTC timezone in
// the JSON body, but YouTube charts does it only in its HTTP headers
ZoneOffset.UTC),
// We don't have more info than the release day
true);
}
@Override
public String getName() {
return videoObject.getString("title");
}
@Override
public String getUrl() throws ParsingException {
return YoutubeStreamLinkHandlerFactory.getInstance().getUrl(
videoObject.getString("id"));
}
@Nonnull
@Override
public List<Image> getThumbnails() throws ParsingException {
return getThumbnailsFromInfoItem(videoObject);
}
}
}

View File

@ -0,0 +1,208 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientVersion;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemLockupExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
abstract class YoutubeDesktopBaseKioskExtractor extends KioskExtractor<StreamInfoItem> {
protected final String browseId;
protected final String params;
protected YoutubeChannelHelper.ChannelResponseData responseData;
protected YoutubeDesktopBaseKioskExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId,
final String browseId,
final String params) {
super(streamingService, linkHandler, kioskId);
this.browseId = browseId;
this.params = params;
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
responseData = YoutubeChannelHelper.getChannelResponse(
browseId,
params,
getExtractorLocalization(),
getExtractorContentCountry());
}
@Nonnull
@Override
public String getName() throws ParsingException {
return YoutubeChannelHelper.getChannelName(
YoutubeChannelHelper.getChannelHeader(responseData.jsonResponse),
null,
responseData.jsonResponse);
}
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
final JsonObject tabRendererContent = responseData.jsonResponse.getObject("contents")
.getObject("twoColumnBrowseResultsRenderer")
.getArray("tabs")
.getObject(0)
.getObject("tabRenderer")
.getObject("content");
final JsonArray tabContents;
if (tabRendererContent.has("sectionListRenderer")) {
tabContents = tabRendererContent.getObject("sectionListRenderer")
.getArray("contents")
.getObject(0)
.getObject("itemSectionRenderer")
.getArray("contents")
.getObject(0)
.getObject("shelfRenderer")
.getObject("content")
.getObject("gridRenderer")
.getArray("items");
} else if (tabRendererContent.has("richGridRenderer")) {
tabContents = tabRendererContent.getObject("richGridRenderer")
.getArray("contents");
} else {
tabContents = new JsonArray();
}
return collectStreamItems(tabContents,
responseData.jsonResponse.getObject("responseContext")
.getString("visitorData"));
}
@Override
public InfoItemsPage<StreamInfoItem> getPage(final Page page)
throws IOException, ExtractionException {
if (page == null || page.getBody() == null) {
throw new IllegalArgumentException("Page is null or doesn't contain a body");
}
final JsonObject continuationResponse = getJsonPostResponse("browse", page.getBody(),
getExtractorLocalization());
final JsonArray continuationItems =
continuationResponse.getArray("onResponseReceivedActions")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(jsonObject -> jsonObject.has("appendContinuationItemsAction"))
.map(jsonObject -> jsonObject.getObject("appendContinuationItemsAction"))
.findFirst()
.orElse(new JsonObject())
.getArray("continuationItems");
// The page ID is the visitor data
return collectStreamItems(continuationItems, page.getId());
}
private InfoItemsPage<StreamInfoItem> collectStreamItems(
@Nonnull final JsonArray items,
@Nullable final String visitorData) throws IOException, ExtractionException {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
final Page nextPage;
if (items.isEmpty()) {
nextPage = null;
} else {
final TimeAgoParser timeAgoParser = getTimeAgoParser();
items.streamAsJsonObjects()
.forEachOrdered(content -> {
if (content.has("richItemRenderer")) {
final JsonObject richItem = content.getObject("richItemRenderer")
.getObject("content");
if (richItem.has("videoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor(
richItem.getObject("videoRenderer"), timeAgoParser));
}
} else if (content.has("gridVideoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor(
content.getObject("gridVideoRenderer"), timeAgoParser));
} else if (content.has("lockupViewModel")) {
// lockupViewModels are not used yet, but may be in the future
final JsonObject lockupViewModel = content.getObject("lockupViewModel");
if ("LOCKUP_CONTENT_TYPE_VIDEO".equals(
lockupViewModel.getString("contentType"))) {
collector.commit(new YoutubeStreamInfoItemLockupExtractor(
lockupViewModel, timeAgoParser));
}
}
});
final JsonObject lastContent = items.getObject(items.size() - 1);
if (lastContent.has("continuationItemRenderer")) {
nextPage = getNextPageFrom(
lastContent.getObject("continuationItemRenderer"), visitorData);
} else {
nextPage = null;
}
}
return new InfoItemsPage<>(collector, nextPage);
}
@Nullable
private Page getNextPageFrom(@Nullable final JsonObject continuation,
@Nullable final String visitorData)
throws IOException, ExtractionException {
if (isNullOrEmpty(continuation)) {
return null;
}
final JsonObject continuationEndpoint = continuation.getObject("continuationEndpoint");
final String continuationToken = continuationEndpoint.getObject("continuationCommand")
.getString("token");
// Visitor data is required to get videos in continuations, so we need to apply it to the
// next page and save it as an ID so it can be applied to future continuations
final InnertubeClientRequestInfo webClientRequestInfo =
InnertubeClientRequestInfo.ofWebClient();
webClientRequestInfo.clientInfo.clientVersion = getClientVersion();
webClientRequestInfo.clientInfo.visitorData = visitorData;
final byte[] body = JsonWriter.string(prepareJsonBuilder(getExtractorLocalization(),
getExtractorContentCountry(),
webClientRequestInfo,
null)
.value("continuation", continuationToken)
.done())
.getBytes(StandardCharsets.UTF_8);
// The URL is not needed and used, it is only provided to make Page.isValid return true
return new Page(YOUTUBEI_V1_URL + "browse?" + DISABLE_PRETTY_PRINT_PARAMETER, visitorData,
null, null, body);
}
}

View File

@ -0,0 +1,14 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
public class YoutubeLiveExtractor extends YoutubeDesktopBaseKioskExtractor {
public YoutubeLiveExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler, kioskId, "UC4R8DWoMoI7CAwX8_LjQHig",
"EgdsaXZldGFikgEDCKEK");
}
}

View File

@ -18,7 +18,7 @@
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.extractor.services.youtube.extractors;
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextAtKey;
@ -36,6 +36,7 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;

View File

@ -0,0 +1,14 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
public class YoutubeTrendingGamingVideosExtractor extends YoutubeDesktopBaseKioskExtractor {
public YoutubeTrendingGamingVideosExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler, kioskId, "UCOpNcN46UbXVtpKMrmU4Abg",
"Egh0cmVuZGluZw%3D%3D");
}
}

View File

@ -0,0 +1,24 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import javax.annotation.Nonnull;
public class YoutubeTrendingMoviesAndShowsTrailersExtractor
extends YoutubeChartsBaseKioskExtractor {
public YoutubeTrendingMoviesAndShowsTrailersExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler, kioskId, "TRENDING_MOVIES");
}
@Nonnull
@Override
public String getName() throws ParsingException {
// This is the official YouTube Charts name, even if shows' trailers are returned too
return "Trending Movie Trailers";
}
}

View File

@ -0,0 +1,39 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.UnsupportedContentInCountryException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import javax.annotation.Nonnull;
import java.io.IOException;
public class YoutubeTrendingMusicExtractor extends YoutubeChartsBaseKioskExtractor {
public YoutubeTrendingMusicExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler, kioskId, "TRENDING_VIDEOS");
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
if (!YT_CHARTS_SUPPORTED_COUNTRY_CODES.contains(
getExtractorContentCountry().getCountryCode())) {
throw new UnsupportedContentInCountryException(
"YouTube Charts doesn't support this country for trending music videos charts");
}
super.onFetchPage(downloader);
}
@Nonnull
@Override
public String getName() throws ParsingException {
// This is the official YouTube Charts name, even if autogenerated tracks and unofficial
// contents are returned
return "Trending Music Videos";
}
}

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
public class YoutubeTrendingPodcastsEpisodesExtractor extends YoutubeDesktopBaseKioskExtractor {
public YoutubeTrendingPodcastsEpisodesExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler, kioskId, "FEpodcasts_destination", "qgcCCAM%3D");
}
}

View File

@ -0,0 +1,53 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isInvidiousURL;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
public final class YoutubeLiveLinkHandlerFactory extends ListLinkHandlerFactory {
public static final String KIOSK_ID = "live";
public static final YoutubeLiveLinkHandlerFactory INSTANCE =
new YoutubeLiveLinkHandlerFactory();
private static final String LIVE_CHANNEL_PATH = "/channel/UC4R8DWoMoI7CAwX8_LjQHig/livetab";
private static final String LIVE_CHANNEL_TAB_PARAMS = "ss=CKEK";
private YoutubeLiveLinkHandlerFactory() {
}
@Override
public String getUrl(final String id,
final List<String> contentFilters,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://www.youtube.com" + LIVE_CHANNEL_PATH + "?" + LIVE_CHANNEL_TAB_PARAMS;
}
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
return KIOSK_ID;
}
@Override
public boolean onAcceptUrl(final String url) {
final URL urlObj;
try {
urlObj = Utils.stringToURL(url);
} catch (final MalformedURLException e) {
return false;
}
return Utils.isHTTP(urlObj) && (isYoutubeURL(urlObj) || isInvidiousURL(urlObj))
&& LIVE_CHANNEL_PATH.equals(urlObj.getPath())
&& LIVE_CHANNEL_TAB_PARAMS.equals(urlObj.getQuery());
}
}

View File

@ -0,0 +1,49 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isInvidiousURL;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
public final class YoutubeTrendingGamingVideosLinkHandlerFactory extends ListLinkHandlerFactory {
public static final String KIOSK_ID = "trending_gaming";
public static final YoutubeTrendingGamingVideosLinkHandlerFactory INSTANCE =
new YoutubeTrendingGamingVideosLinkHandlerFactory();
private YoutubeTrendingGamingVideosLinkHandlerFactory() {
}
@Override
public String getUrl(final String id,
final List<String> contentFilters,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://www.youtube.com/gaming/trending";
}
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
return KIOSK_ID;
}
@Override
public boolean onAcceptUrl(final String url) {
final URL urlObj;
try {
urlObj = Utils.stringToURL(url);
} catch (final MalformedURLException e) {
return false;
}
return Utils.isHTTP(urlObj) && (isYoutubeURL(urlObj) || isInvidiousURL(urlObj))
&& "/gaming/trending".equals(urlObj.getPath());
}
}

View File

@ -33,16 +33,12 @@ import java.util.List;
public final class YoutubeTrendingLinkHandlerFactory extends ListLinkHandlerFactory {
private static final YoutubeTrendingLinkHandlerFactory INSTANCE =
public static final YoutubeTrendingLinkHandlerFactory INSTANCE =
new YoutubeTrendingLinkHandlerFactory();
private YoutubeTrendingLinkHandlerFactory() {
}
public static YoutubeTrendingLinkHandlerFactory getInstance() {
return INSTANCE;
}
public String getUrl(final String id,
final List<String> contentFilters,
final String sortFilter)

View File

@ -0,0 +1,51 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Locale;
public final class YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory
extends ListLinkHandlerFactory {
public static final String KIOSK_ID = "trending_movies_and_shows";
public static final YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory INSTANCE =
new YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory();
private static final String PATH = "/charts/TrendingTrailers";
private YoutubeTrendingMoviesAndShowsTrailersLinkHandlerFactory() {
}
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://charts.youtube.com" + PATH;
}
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
return KIOSK_ID;
}
@Override
public boolean onAcceptUrl(final String url) throws ParsingException {
final URL urlObj;
try {
urlObj = Utils.stringToURL(url);
} catch (final MalformedURLException e) {
return false;
}
return Utils.isHTTP(urlObj)
&& "charts.youtube.com".equals(urlObj.getHost().toLowerCase(Locale.ROOT))
&& PATH.equals(urlObj.getPath());
}
}

View File

@ -0,0 +1,52 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Locale;
public final class YoutubeTrendingMusicLinkHandlerFactory
extends ListLinkHandlerFactory {
public static final String KIOSK_ID = "trending_music";
public static final YoutubeTrendingMusicLinkHandlerFactory INSTANCE =
new YoutubeTrendingMusicLinkHandlerFactory();
private static final String PATH = "/charts/TrendingVideos";
private YoutubeTrendingMusicLinkHandlerFactory() {
}
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://charts.youtube.com" + PATH + "/RightNow";
}
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
return KIOSK_ID;
}
@Override
public boolean onAcceptUrl(final String url) throws ParsingException {
final URL urlObj;
try {
urlObj = Utils.stringToURL(url);
} catch (final MalformedURLException e) {
return false;
}
return Utils.isHTTP(urlObj)
&& "charts.youtube.com".equals(urlObj.getHost().toLowerCase(Locale.ROOT))
// Accept URLs not containing the /RightNow part
&& urlObj.getPath().startsWith(PATH);
}
}

View File

@ -0,0 +1,52 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isInvidiousURL;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
public final class YoutubeTrendingPodcastsEpisodesLinkHandlerFactory
extends ListLinkHandlerFactory {
public static final String KIOSK_ID = "trending_podcasts_episodes";
public static final YoutubeTrendingPodcastsEpisodesLinkHandlerFactory INSTANCE =
new YoutubeTrendingPodcastsEpisodesLinkHandlerFactory();
private static final String PATH = "/podcasts/popularepisodes";
private YoutubeTrendingPodcastsEpisodesLinkHandlerFactory() {
}
@Override
public String getUrl(final String id,
final List<String> contentFilters,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://www.youtube.com" + PATH;
}
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
return KIOSK_ID;
}
@Override
public boolean onAcceptUrl(final String url) {
final URL urlObj;
try {
urlObj = Utils.stringToURL(url);
} catch (final MalformedURLException e) {
return false;
}
return Utils.isHTTP(urlObj) && (isYoutubeURL(urlObj) || isInvidiousURL(urlObj))
&& PATH.equals(urlObj.getPath());
}
}

View File

@ -3,22 +3,305 @@ package org.schabi.newpipe.extractor.services.youtube;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.services.DefaultTests.assertNoMoreItems;
import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestMoreItems;
import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestRelatedItems;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.BaseListExtractorTest;
import org.schabi.newpipe.extractor.services.DefaultSimpleExtractorTest;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeTrendingExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeLiveExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingGamingVideosExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingMoviesAndShowsTrailersExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingMusicExtractor;
import org.schabi.newpipe.extractor.services.youtube.extractors.kiosk.YoutubeTrendingPodcastsEpisodesExtractor;
public class YoutubeKioskExtractorTest {
public static class Live extends DefaultSimpleExtractorTest<YoutubeLiveExtractor>
implements BaseListExtractorTest, InitYoutubeTest {
@Override
protected YoutubeLiveExtractor createExtractor() throws Exception {
return (YoutubeLiveExtractor) YouTube.getKioskList().getDefaultKioskExtractor();
}
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor().getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertEquals("Live", extractor().getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("live", extractor().getId());
}
@Override
@Test
public void testUrl() throws Exception {
assertEquals(
"https://www.youtube.com/channel/UC4R8DWoMoI7CAwX8_LjQHig/livetab?ss=CKEK",
extractor().getUrl());
}
@Override
@Test
public void testOriginalUrl() throws Exception {
assertEquals(
"https://www.youtube.com/channel/UC4R8DWoMoI7CAwX8_LjQHig/livetab?ss=CKEK",
extractor().getOriginalUrl());
}
@Override
@Test
public void testRelatedItems() throws Exception {
// As there is sometimes very recently ended livestreams present, we can't test whether
// all streams are running live streams
defaultTestRelatedItems(extractor());
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
defaultTestMoreItems(extractor());
}
}
public static class TrendingPodcastsEpisodes extends
DefaultSimpleExtractorTest<YoutubeTrendingPodcastsEpisodesExtractor>
implements BaseListExtractorTest, InitYoutubeTest {
@Override
protected YoutubeTrendingPodcastsEpisodesExtractor createExtractor() throws Exception {
return (YoutubeTrendingPodcastsEpisodesExtractor) YouTube.getKioskList()
.getExtractorById("trending_podcasts_episodes", null);
}
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor().getServiceId());
}
@Override
@Test
public void testName() throws Exception {
// The name is the title of channel and not of the section
assertEquals("Podcasts", extractor().getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("trending_podcasts_episodes", extractor().getId());
}
@Override
@Test
public void testUrl() throws Exception {
assertEquals("https://www.youtube.com/podcasts/popularepisodes", extractor().getUrl());
}
@Override
@Test
public void testOriginalUrl() throws Exception {
assertEquals("https://www.youtube.com/podcasts/popularepisodes",
extractor().getOriginalUrl());
}
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor());
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
assertNoMoreItems(extractor());
}
}
public static class TrendingGamingVideos extends
DefaultSimpleExtractorTest<YoutubeTrendingGamingVideosExtractor>
implements BaseListExtractorTest, InitYoutubeTest {
@Override
protected YoutubeTrendingGamingVideosExtractor createExtractor() throws Exception {
return (YoutubeTrendingGamingVideosExtractor) YouTube.getKioskList()
.getExtractorById("trending_gaming", null);
}
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor().getServiceId());
}
@Override
@Test
public void testName() throws Exception {
// The name is the title of channel and not of the section
assertEquals("Gaming", extractor().getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("trending_gaming", extractor().getId());
}
@Override
@Test
public void testUrl() throws Exception {
assertEquals("https://www.youtube.com/gaming/trending", extractor().getUrl());
}
@Override
@Test
public void testOriginalUrl() throws Exception {
assertEquals("https://www.youtube.com/gaming/trending", extractor().getOriginalUrl());
}
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor());
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
assertNoMoreItems(extractor());
}
}
public static class TrendingMoviesAndShowsTrailers extends
DefaultSimpleExtractorTest<YoutubeTrendingMoviesAndShowsTrailersExtractor>
implements BaseListExtractorTest, InitYoutubeTest {
@Override
protected YoutubeTrendingMoviesAndShowsTrailersExtractor createExtractor() throws Exception {
return (YoutubeTrendingMoviesAndShowsTrailersExtractor) YouTube.getKioskList()
.getExtractorById("trending_movies_and_shows", null);
}
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor().getServiceId());
}
@Override
@Test
public void testName() throws Exception {
// The title is hardcoded in the extractor, as InnerTube responses don't provide it
// (handled client-side)
assertEquals("Trending Movie Trailers", extractor().getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("trending_movies_and_shows", extractor().getId());
}
@Override
@Test
public void testUrl() throws Exception {
assertEquals("https://charts.youtube.com/charts/TrendingTrailers",
extractor().getUrl());
}
@Override
@Test
public void testOriginalUrl() throws Exception {
assertEquals("https://charts.youtube.com/charts/TrendingTrailers",
extractor().getOriginalUrl());
}
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor());
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
assertNoMoreItems(extractor());
}
}
public static class TrendingMusic extends
DefaultSimpleExtractorTest<YoutubeTrendingMusicExtractor>
implements BaseListExtractorTest, InitYoutubeTest {
@Override
protected YoutubeTrendingMusicExtractor createExtractor() throws Exception {
return (YoutubeTrendingMusicExtractor) YouTube.getKioskList()
.getExtractorById("trending_music", null);
}
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor().getServiceId());
}
@Override
@Test
public void testName() throws Exception {
// The title is hardcoded in the extractor, as InnerTube responses don't provide it
// (handled client-side)
assertEquals("Trending Music Videos", extractor().getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("trending_music", extractor().getId());
}
@Override
@Test
public void testUrl() throws Exception {
assertEquals("https://charts.youtube.com/charts/TrendingVideos/RightNow",
extractor().getUrl());
}
@Override
@Test
public void testOriginalUrl() throws Exception {
assertEquals("https://charts.youtube.com/charts/TrendingVideos/RightNow",
extractor().getOriginalUrl());
}
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor());
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
assertNoMoreItems(extractor());
}
}
// Deprecated (i.e. removed from the interface of YouTube) since July 21, 2025
public static class Trending extends DefaultSimpleExtractorTest<YoutubeTrendingExtractor>
implements BaseListExtractorTest, InitYoutubeTest {
implements BaseListExtractorTest, InitYoutubeTest {
@Override
protected YoutubeTrendingExtractor createExtractor() throws Exception {
return (YoutubeTrendingExtractor) YouTube.getKioskList().getDefaultKioskExtractor();
return (YoutubeTrendingExtractor) YouTube.getKioskList().getExtractorById(
"Trending", null);
}
@Override

View File

@ -69,7 +69,7 @@ public class YoutubeServiceTest {
@Test
void testGetDefaultKiosk() throws Exception {
assertEquals("Trending", kioskList.getDefaultKioskExtractor(null).getId());
assertEquals("live", kioskList.getDefaultKioskExtractor(null).getId());
}

View File

@ -0,0 +1,96 @@
{
"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-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-security-policy-report-only": [
"script-src \u0027unsafe-eval\u0027 \u0027self\u0027 \u0027unsafe-inline\u0027 https://www.google.com https://apis.google.com https://ssl.gstatic.com https://www.gstatic.com https://www.googletagmanager.com https://www.google-analytics.com https://*.youtube.com https://*.google.com https://*.gstatic.com https://youtube.com https://www.youtube.com https://google.com https://*.doubleclick.net https://*.googleapis.com https://www.googleadservices.com https://tpc.googlesyndication.com https://www.youtubekids.com;report-uri /cspreport/allowlist"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Thu, 31 Jul 2025 19:06:56 GMT"
],
"document-policy": [
"include-js-call-stacks-in-crash-reports"
],
"expires": [
"Thu, 31 Jul 2025 19:06:56 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"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-factors\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\"}]}"
],
"reporting-endpoints": [
"default\u003d\"/web-reports?context\u003deJwNz1tIk3EYBvA-HxLdp9_3_V8vRA0yETroZGpTKC0oPGBpBoXiVi51m4e5mW1rlt2EWJZIYeUhkyRIEZQICWRaXQRS0AEPF5nSwQ4YonWhiWb2XvyuHt6H59V1bZ2KrpJqNqul2Gyn9OjSWcm8s1bKeHtOas50SysP3dKA3y0tRHslU4xXap_ySo6PPml9zid1a4UBtsjCAB_zF-lgs-mw3KHD7xkd6pZ0MMXJUBJkEJvIkbGQJ6PKLONJrwz_YxmzP2R4TCH4ZQmB0RMC10oI5vNDMdoRivGJUMSfUJDWrKBvWEH-mIKbzP5XweKGgpR0FYNZKrxWFf0OFbJXRWeTisQ2FVksqEdF-CsVo7EagiwadMx4W8P4XQ1drzXM_NGwf03DqTABzx6Bkb0CFqNAyj6BhoMCXScFZosFcu0CpZUCPXUC9fWcsfhrAm-aBQ61CCy1CiTdEjhwR-BZv8CHAYGoQYHNpwJhkwLT7wW2zQukLgl8WRUoXxew_xO4sYWgBBOuhxOqIwjPIwnDUYT27YQu9i2GMLqL8H03ITmZYGSBRoIljdB7mDCXQRjKJEyzuGzCZg5h5Qih6CjBzE4zCythZczGylklczAnq2G1zM28zMcusHp27xh35hMajxN-FhByTYTPZr4pJtjt3MEm2TpbcxGCPbzxIiHiMmGV7Wjgm0bCIku_Qmi4yvubCAWtBLQRljs57-afH_C-Xv67j_BpgJDpJ-SNENQxQstLAslB7zYGXwRq94e-Bsbo61wet6fEmnDeWqK31bqcbr3VWaYvra1wV5SecRQnGZKMhtRkQ0KiobjG8B-xPcol\""
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003d8-LUGALaWbw; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 04-Nov-2022 19:06:56 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"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"
}
}

View File

@ -0,0 +1,96 @@
{
"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-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-security-policy-report-only": [
"script-src \u0027unsafe-eval\u0027 \u0027self\u0027 \u0027unsafe-inline\u0027 https://www.google.com https://apis.google.com https://ssl.gstatic.com https://www.gstatic.com https://www.googletagmanager.com https://www.google-analytics.com https://*.youtube.com https://*.google.com https://*.gstatic.com https://youtube.com https://www.youtube.com https://google.com https://*.doubleclick.net https://*.googleapis.com https://www.googleadservices.com https://tpc.googlesyndication.com https://www.youtubekids.com;report-uri /cspreport/allowlist"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Thu, 31 Jul 2025 19:09:29 GMT"
],
"document-policy": [
"include-js-call-stacks-in-crash-reports"
],
"expires": [
"Thu, 31 Jul 2025 19:09:29 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"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-factors\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\"}]}"
],
"reporting-endpoints": [
"default\u003d\"/web-reports?context\u003deJwNzl1Ik3EUx3H__kT0ebY9z_8YhBokIhTZZM6mVFpQtETUDArEWZsv23ydtrY1yyBCLEGi6EUtC7xJKZTwIpBpEUhRUIHmRab0CiWEq4tMWmnn4nNzOIfzVb4lzGU0i3OxZtG63iqyin3iwdkTonqLXxx4fVL02gNi5W5AjEYC4ntGSDgyQ6J_LiRa3odF7HNY3NEr4z1plfFhFqlS4PEo-DWg4OeCgo6oAke2CmOOCp3Nlqj4Xq7i4bCKyLiKxa8qgg4DfrgMsAUNaFsxYKnCiKkBI2Zmjdh-xITCXhNGJkyoeGbCFeb9a8LyPxPyizSMHdQQcmtQQxpu9mjI7dNgZ0lDGja-0DCVpSPJpUNhtus6Zm7pGHypY-G3jt1_dBxLkQhuk5jcIVFjk8jfJdG1V2LwqMSiU6LMK1HXJNHZyXP2qldi3yWJ6FUJ6zWJPTckHt-TyLgv8W5UIn1MYv2RRMobifm3EpuWJAqiEp9WJRpiEt41ictxBFMyIbqB0JpKeJJGmEgn9G8mDLKprYS8PIKNJdoIrkLC8H7CvJ2QXUwYZ-slhKpSQjU7zlysltUzD2tgTayF-Vg787MAC7EwO8062e1DhLjDhG5W5iB8rOZ9J8Hr5XsWY8lB7jlDSD1PWGWl3YRlVnSB8IV1XeTWHkJCHzcME_pGCB9G-fc4wR4hlE8S8p8TyJC0tDY2naivPh2ajs80d7QFA8Fad84pd63Z42_zBcxuX725zt8YaKyraXFaLVabpcC6MyfX4my3_Ad2QbxV\""
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dfGO-_uyQPYc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 04-Nov-2022 19:09:29 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"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"
}
}

View File

@ -0,0 +1,96 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Origin": [
"https://www.youtube.com"
],
"Referer": [
"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-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-security-policy-report-only": [
"script-src \u0027unsafe-eval\u0027 \u0027self\u0027 \u0027unsafe-inline\u0027 https://www.google.com https://apis.google.com https://ssl.gstatic.com https://www.gstatic.com https://www.googletagmanager.com https://www.google-analytics.com https://*.youtube.com https://*.google.com https://*.gstatic.com https://youtube.com https://www.youtube.com https://google.com https://*.doubleclick.net https://*.googleapis.com https://www.googleadservices.com https://tpc.googlesyndication.com https://www.youtubekids.com;report-uri /cspreport/allowlist"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Thu, 31 Jul 2025 19:09:22 GMT"
],
"document-policy": [
"include-js-call-stacks-in-crash-reports"
],
"expires": [
"Thu, 31 Jul 2025 19:09:22 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"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-factors\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\"}]}"
],
"reporting-endpoints": [
"default\u003d\"/web-reports?context\u003deJwNzltIk3EYBnD_PiG60_f9X6NQg8SEIpvM2ZRKC4qWSCuDAnHW5mGbmk5b39YqgwixBJGig8eKCJoUSnQRyLS6CDrQATxcZEplFyFYVpBKmvZe_G5enufl0S2tGks9LgIrdSK9wC8enjshSjcGRLg5IPa-PykcazTRatfE3D1N9EU10fUmKGZSQ8KZFhIdYyFR-yksFr-GxS21ONabXBwbZtESHbxeHf506vBrQofwrA7ODD2MmXpINlKox8wBPR5H9Ig-0mPymx5BpwE_3QbYggbUzxkwXWTEUKcRwyNGbDlsQl6rCb0DJhS9MOEK8y2Z8OOfCTn5Cvr3KQh5FOhDCrpaFGS1K7Cz-DsKNkQUrH2tYChdRbxbhY7ZrqsY7lbR81bFxLyKHX9VHE2U0DZL3M2QGNwqUWaTaGM52yWadkn0HJGYdEk4fBIVNRKNjXxn71oldrdJzF6VsF6T2HlD4ul9idQHEh_7JFL6JVaeSCSOSox_kFg3LZE7KzG1IFG1KOFblrgcQzAlEH6vJtQlEZ4lEwZSCB3rCd1saBMhO5tgY3E2gjuPENlDGLcTMgoIK4WEkv2EUnaMuVk5q2ReVsVqWC3zswYWYBoLsTA7wxrZzYOEmEOEZuZwEr6Uct5F8Pm4zxZZQpC3nCWI84SkC4QF5mgmfGf5FwlNl3hnCwHthJ7bvCNCaO8lfO4j2KOcHeSfrO0VgQzxU8v9z-PUl_MzoyLNfLo-qAXLPZmnPOVmb6Der5k9_kpzRaBaq64oq3VZLVabJde6LTPL4mqw_AeG9sFO\""
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dXasVls-aAZw; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 04-Nov-2022 19:09:22 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"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"
}
}