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:
commit
0a7b72aec6
@ -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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -69,7 +69,7 @@ public class YoutubeServiceTest {
|
||||
|
||||
@Test
|
||||
void testGetDefaultKiosk() throws Exception {
|
||||
assertEquals("Trending", kioskList.getDefaultKioskExtractor(null).getId());
|
||||
assertEquals("live", kioskList.getDefaultKioskExtractor(null).getId());
|
||||
}
|
||||
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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"
|
||||
}
|
||||
}
|
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