Merge branch 'dev'

This commit is contained in:
Stypox 2024-07-25 18:29:07 +02:00
commit 176da72cb4
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
434 changed files with 34012 additions and 31935 deletions

View File

@ -32,7 +32,7 @@ jobs:
run: ./gradlew aggregatedJavadocs
- name: Deploy JavaDocs
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/docs

View File

@ -21,7 +21,7 @@ If you're using Gradle, you could add NewPipe Extractor as a dependency with the
-dontwarn org.mozilla.javascript.tools.**
```
**Note:** To use NewPipe Extractor in Android projects with a `minSdk` below 26, [API desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) is required. If the `minSdk` is below 19, the `desugar_jdk_libs_nio` artifact is required, which requires Android Gradle Plugin (AGP) version 7.4.0.
**Note:** To use NewPipe Extractor in Android projects with a `minSdk` below 33, [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) with the `desugar_jdk_libs_nio` artifact is required.
### Testing changes

View File

@ -8,7 +8,7 @@ allprojects {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
version 'v0.24.0'
version 'v0.24.2'
group 'com.github.TeamNewPipe'
repositories {
@ -28,8 +28,8 @@ allprojects {
ext {
nanojsonVersion = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
spotbugsVersion = "4.8.3"
junitVersion = "5.10.2"
spotbugsVersion = "4.8.6"
junitVersion = "5.10.3"
checkstyleVersion = "10.4"
}
}

View File

@ -31,7 +31,7 @@ dependencies {
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation 'org.mozilla:rhino:1.7.13'
implementation 'org.mozilla:rhino:1.7.15'
checkstyle "com.puppycrawl.tools:checkstyle:$checkstyleVersion"
@ -41,5 +41,5 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation "com.squareup.okhttp3:okhttp:3.12.13"
testImplementation 'com.google.code.gson:gson:2.10.1'
testImplementation 'com.google.code.gson:gson:2.11.0'
}

View File

@ -16,6 +16,7 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.utils.ImageSuffix;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -155,25 +156,34 @@ public final class BandcampExtractorHelper {
/**
* @return <code>true</code> if the given URL looks like it comes from a bandcamp custom domain
* or if it comes from <code>bandcamp.com</code> itself
* or a <code>*.bandcamp.com</code> subdomain
*/
public static boolean isSupportedDomain(final String url) throws ParsingException {
public static boolean isArtistDomain(final String url) throws ParsingException {
// Accept all bandcamp.com URLs
if (url.toLowerCase().matches("https?://.+\\.bandcamp\\.com(/.*)?")) {
return true;
}
// Reject non-artist bandcamp.com URLs
if (url.toLowerCase().matches("https?://bandcamp\\.com(/.*)?")) {
return false;
}
try {
// Test other URLs for whether they contain a footer that links to bandcamp
return Jsoup.parse(NewPipe.getDownloader().get(url).responseBody())
.getElementById("pgFt")
.getElementById("pgFt-inner")
.getElementById("footer-logo-wrapper")
.getElementById("footer-logo")
.getElementsByClass("hiddenAccess")
.text().equals("Bandcamp");
} catch (final NullPointerException e) {
return Jsoup.parse(
NewPipe.getDownloader()
.get(Utils.replaceHttpWithHttps(url))
.responseBody()
)
.getElementsByClass("cart-wrapper")
.get(0)
.getElementsByTag("a")
.get(0)
.attr("href")
.equals("https://bandcamp.com/cart");
} catch (final NullPointerException | IndexOutOfBoundsException e) {
return false;
} catch (final IOException | ReCaptchaException e) {
throw new ParsingException("Could not determine whether URL is custom domain "

View File

@ -24,7 +24,7 @@ import static org.schabi.newpipe.extractor.services.bandcamp.extractors.Bandcamp
public class BandcampRadioExtractor extends KioskExtractor<StreamInfoItem> {
public static final String KIOSK_RADIO = "Radio";
public static final String RADIO_API_URL = BASE_API_URL + "/bcweekly/1/list";
public static final String RADIO_API_URL = BASE_API_URL + "/bcweekly/3/list";
private JsonObject json = null;

View File

@ -35,6 +35,12 @@ public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
return 0;
}
@Nullable
@Override
public String getShortDescription() {
return show.getString("desc");
}
@Nullable
@Override
public String getTextualUploadDate() {
@ -75,8 +81,8 @@ public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
@Override
public String getUploaderName() {
// JSON does not contain uploader name
return "";
// The "title" field contains the title of the series, e.g. "Bandcamp Weekly".
return show.getString("title");
}
@Override

View File

@ -43,7 +43,12 @@ public class BandcampPlaylistStreamInfoItemExtractor extends BandcampStreamInfoI
@Override
public String getUrl() {
return getUploaderUrl() + track.getString("title_link");
final String relativeUrl = track.getString("title_link");
if (relativeUrl != null) {
return getUploaderUrl() + relativeUrl;
} else {
return null;
}
}
@Override
@ -66,7 +71,7 @@ public class BandcampPlaylistStreamInfoItemExtractor extends BandcampStreamInfoI
@Nonnull
@Override
public List<Image> getThumbnails() throws ParsingException {
if (substituteCovers.isEmpty()) {
if (substituteCovers.isEmpty() && getUrl() != null) {
try {
final StreamExtractor extractor = service.getStreamExtractor(getUrl());
extractor.fetchPage();

View File

@ -33,7 +33,8 @@ public final class BandcampChannelLinkHandlerFactory extends ListLinkHandlerFact
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
try {
final String response = NewPipe.getDownloader().get(url).responseBody();
final String response = NewPipe.getDownloader().get(Utils.replaceHttpWithHttps(url))
.responseBody();
// Use band data embedded in website to extract ID
final JsonObject bandData = JsonUtils.getJsonData(response, "data-band");
@ -90,7 +91,7 @@ public final class BandcampChannelLinkHandlerFactory extends ListLinkHandlerFact
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(lowercaseUrl);
return BandcampExtractorHelper.isArtistDomain(lowercaseUrl);
}
}
}

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import org.schabi.newpipe.extractor.utils.Utils;
import java.util.List;
@ -24,7 +25,7 @@ public final class BandcampCommentsLinkHandlerFactory extends ListLinkHandlerFac
@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
return url;
return Utils.replaceHttpWithHttps(url);
}
@Override
@ -39,7 +40,7 @@ public final class BandcampCommentsLinkHandlerFactory extends ListLinkHandlerFac
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(url);
return BandcampExtractorHelper.isArtistDomain(url);
}
@Override
@ -47,6 +48,6 @@ public final class BandcampCommentsLinkHandlerFactory extends ListLinkHandlerFac
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return id;
return Utils.replaceHttpWithHttps(id);
}
}

View File

@ -5,6 +5,7 @@ package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import org.schabi.newpipe.extractor.utils.Utils;
import java.util.List;
@ -33,7 +34,7 @@ public final class BandcampPlaylistLinkHandlerFactory extends ListLinkHandlerFac
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
return url;
return Utils.replaceHttpWithHttps(url);
}
/**
@ -48,6 +49,6 @@ public final class BandcampPlaylistLinkHandlerFactory extends ListLinkHandlerFac
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(url);
return BandcampExtractorHelper.isArtistDomain(url);
}
}

View File

@ -8,7 +8,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.UnsupportedEncodingException;
import java.util.List;
public final class BandcampSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
@ -28,10 +27,6 @@ public final class BandcampSearchQueryHandlerFactory extends SearchQueryHandlerF
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
try {
return BASE_URL + "/search?q=" + Utils.encodeUrlUtf8(query) + "&page=1";
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("query \"" + query + "\" could not be encoded", e);
}
return BASE_URL + "/search?q=" + Utils.encodeUrlUtf8(query) + "&page=1";
}
}

View File

@ -5,6 +5,7 @@ package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import org.schabi.newpipe.extractor.utils.Utils;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
@ -49,7 +50,7 @@ public final class BandcampStreamLinkHandlerFactory extends LinkHandlerFactory {
if (input.matches("\\d+")) {
return BASE_URL + "/?show=" + input;
} else {
return input;
return Utils.replaceHttpWithHttps(input);
}
}
@ -71,6 +72,6 @@ public final class BandcampStreamLinkHandlerFactory extends LinkHandlerFactory {
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(url);
return BandcampExtractorHelper.isArtistDomain(url);
}
}

View File

@ -4,7 +4,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.UnsupportedEncodingException;
import java.util.List;
public final class MediaCCCSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
@ -42,10 +41,6 @@ public final class MediaCCCSearchQueryHandlerFactory extends SearchQueryHandlerF
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
try {
return "https://media.ccc.de/public/events/search?q=" + Utils.encodeUrlUtf8(query);
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("Could not create search string with query: " + query, e);
}
return "https://media.ccc.de/public/events/search?q=" + Utils.encodeUrlUtf8(query);
}
}

View File

@ -38,7 +38,6 @@ import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -379,8 +378,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
}
@Nonnull
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
throws UnsupportedEncodingException {
private String getRelatedItemsUrl(@Nonnull final List<String> tags) {
final String url = baseUrl + PeertubeSearchQueryHandlerFactory.SEARCH_ENDPOINT_VIDEOS;
final StringBuilder params = new StringBuilder();
params.append("start=0&count=8&sort=-createdAt");

View File

@ -5,7 +5,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.UnsupportedEncodingException;
import java.util.List;
public final class PeertubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
@ -49,21 +48,17 @@ public final class PeertubeSearchQueryHandlerFactory extends SearchQueryHandlerF
final String sortFilter,
final String baseUrl)
throws ParsingException, UnsupportedOperationException {
try {
final String endpoint;
if (contentFilters.isEmpty()
|| contentFilters.get(0).equals(VIDEOS)
|| contentFilters.get(0).equals(SEPIA_VIDEOS)) {
endpoint = SEARCH_ENDPOINT_VIDEOS;
} else if (contentFilters.get(0).equals(CHANNELS)) {
endpoint = SEARCH_ENDPOINT_CHANNELS;
} else {
endpoint = SEARCH_ENDPOINT_PLAYLISTS;
}
return baseUrl + endpoint + "?search=" + Utils.encodeUrlUtf8(searchString);
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("Could not encode query", e);
final String endpoint;
if (contentFilters.isEmpty()
|| contentFilters.get(0).equals(VIDEOS)
|| contentFilters.get(0).equals(SEPIA_VIDEOS)) {
endpoint = SEARCH_ENDPOINT_VIDEOS;
} else if (contentFilters.get(0).equals(CHANNELS)) {
endpoint = SEARCH_ENDPOINT_CHANNELS;
} else {
endpoint = SEARCH_ENDPOINT_PLAYLISTS;
}
return baseUrl + endpoint + "?search=" + Utils.encodeUrlUtf8(searchString);
}
@Override

View File

@ -45,6 +45,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.regex.Pattern;
public final class SoundcloudParsingHelper {
// CHECKSTYLE:OFF
@ -88,6 +89,10 @@ public final class SoundcloudParsingHelper {
private static String clientId;
public static final String SOUNDCLOUD_API_V2_URL = "https://api-v2.soundcloud.com/";
private static final Pattern ON_URL_PATTERN = Pattern.compile(
"^https?://on.soundcloud.com/[0-9a-zA-Z]+$"
);
private SoundcloudParsingHelper() {
}
@ -185,8 +190,21 @@ public final class SoundcloudParsingHelper {
*/
public static String resolveIdWithWidgetApi(final String urlString) throws IOException,
ParsingException {
// Remove the tailing slash from URLs due to issues with the SoundCloud API
String fixedUrl = urlString;
// if URL is an on.soundcloud link, do a request to resolve the redirect
if (ON_URL_PATTERN.matcher(fixedUrl).find()) {
try {
fixedUrl = NewPipe.getDownloader().head(fixedUrl).latestUrl();
// remove tracking params which are in the query string
fixedUrl = fixedUrl.split("\\?")[0];
} catch (final ExtractionException e) {
throw new ParsingException("Could not follow on.soundcloud.com redirect", e);
}
}
// Remove the tailing slash from URLs due to issues with the SoundCloud API
if (fixedUrl.charAt(fixedUrl.length() - 1) == '/') {
fixedUrl = fixedUrl.substring(0, fixedUrl.length() - 1);
}

View File

@ -23,7 +23,6 @@ import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.utils.Parser;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
@ -159,7 +158,7 @@ public class SoundcloudSearchExtractor extends SearchExtractor {
private int getOffsetFromUrl(final String url) throws ParsingException {
try {
return Integer.parseInt(Parser.compatParseMap(new URL(url).getQuery()).get("offset"));
} catch (MalformedURLException | UnsupportedEncodingException e) {
} catch (final MalformedURLException e) {
throw new ParsingException("Could not get offset from page URL", e);
}
}

View File

@ -39,7 +39,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -317,14 +316,6 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
}
}
private static String urlEncode(final String value) {
try {
return Utils.encodeUrlUtf8(value);
} catch (final UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
@Override
public List<VideoStream> getVideoStreams() {
return Collections.emptyList();
@ -344,9 +335,8 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
@Override
public StreamInfoItemsCollector getRelatedItems() throws IOException, ExtractionException {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
final String apiUrl = SOUNDCLOUD_API_V2_URL + "tracks/" + urlEncode(getId())
+ "/related?client_id=" + urlEncode(clientId());
final String apiUrl = SOUNDCLOUD_API_V2_URL + "tracks/" + Utils.encodeUrlUtf8(getId())
+ "/related?client_id=" + Utils.encodeUrlUtf8(clientId());
SoundcloudParsingHelper.getStreamsFromApi(collector, apiUrl);
return collector;

View File

@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public final class SoundcloudSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
@ -60,9 +59,6 @@ public final class SoundcloudSearchQueryHandlerFactory extends SearchQueryHandle
return url + "?q=" + Utils.encodeUrlUtf8(id)
+ "&client_id=" + SoundcloudParsingHelper.clientId()
+ "&limit=" + ITEMS_PER_PAGE + "&offset=0";
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("Could not encode query", e);
} catch (final ReCaptchaException e) {
throw new ParsingException("ReCaptcha required", e);
} catch (final IOException | ExtractionException e) {

View File

@ -9,7 +9,8 @@ import org.schabi.newpipe.extractor.utils.Utils;
public final class SoundcloudStreamLinkHandlerFactory extends LinkHandlerFactory {
private static final SoundcloudStreamLinkHandlerFactory INSTANCE
= new SoundcloudStreamLinkHandlerFactory();
private static final String URL_PATTERN = "^https?://(www\\.|m\\.)?soundcloud.com/[0-9a-z_-]+"
private static final String URL_PATTERN = "^https?://(www\\.|m\\.|on\\.)?"
+ "soundcloud.com/[0-9a-z_-]+"
+ "/(?!(tracks|albums|sets|reposts|followers|following)/?$)[0-9a-z_-]+/?([#?].*)?$";
private static final String API_URL_PATTERN = "^https?://api-v2\\.soundcloud.com"
+ "/(tracks|albums|sets|reposts|followers|following)/([0-9a-z_-]+)/";

View File

@ -4,16 +4,19 @@ import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.defaultAlertsCheck;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@ -21,6 +24,19 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
* Shared functions for extracting YouTube channel pages and tabs.
*/
public final class YoutubeChannelHelper {
private static final String BROWSE_ENDPOINT = "browseEndpoint";
private static final String BROWSE_ID = "browseId";
private static final String CAROUSEL_HEADER_RENDERER = "carouselHeaderRenderer";
private static final String C4_TABBED_HEADER_RENDERER = "c4TabbedHeaderRenderer";
private static final String CONTENT = "content";
private static final String CONTENTS = "contents";
private static final String HEADER = "header";
private static final String PAGE_HEADER_VIEW_MODEL = "pageHeaderViewModel";
private static final String TAB_RENDERER = "tabRenderer";
private static final String TITLE = "title";
private static final String TOPIC_CHANNEL_DETAILS_RENDERER = "topicChannelDetailsRenderer";
private YoutubeChannelHelper() {
}
@ -64,8 +80,8 @@ public final class YoutubeChannelHelper {
.getObject("webCommandMetadata")
.getString("webPageType", "");
final JsonObject browseEndpoint = endpoint.getObject("browseEndpoint");
final String browseId = browseEndpoint.getString("browseId", "");
final JsonObject browseEndpoint = endpoint.getObject(BROWSE_ENDPOINT);
final String browseId = browseEndpoint.getString(BROWSE_ID, "");
if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL")
@ -140,7 +156,7 @@ public final class YoutubeChannelHelper {
while (level < 3) {
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
localization, country)
.value("browseId", id)
.value(BROWSE_ID, id)
.value("params", parameters)
.done())
.getBytes(StandardCharsets.UTF_8);
@ -159,8 +175,8 @@ public final class YoutubeChannelHelper {
.getObject("webCommandMetadata")
.getString("webPageType", "");
final String browseId = endpoint.getObject("browseEndpoint")
.getString("browseId", "");
final String browseId = endpoint.getObject(BROWSE_ENDPOINT)
.getString(BROWSE_ID, "");
if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL")
@ -257,7 +273,7 @@ public final class YoutubeChannelHelper {
* A {@code pageHeaderRenderer} channel header type.
*
* <p>
* This header returns only the channel's name and its avatar.
* This header returns only the channel's name and its avatar for system channels.
* </p>
*/
PAGE
@ -294,20 +310,20 @@ public final class YoutubeChannelHelper {
@Nonnull
public static Optional<ChannelHeader> getChannelHeader(
@Nonnull final JsonObject channelResponse) {
final JsonObject header = channelResponse.getObject("header");
final JsonObject header = channelResponse.getObject(HEADER);
if (header.has("c4TabbedHeaderRenderer")) {
return Optional.of(header.getObject("c4TabbedHeaderRenderer"))
if (header.has(C4_TABBED_HEADER_RENDERER)) {
return Optional.of(header.getObject(C4_TABBED_HEADER_RENDERER))
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.C4_TABBED));
} else if (header.has("carouselHeaderRenderer")) {
return header.getObject("carouselHeaderRenderer")
.getArray("contents")
} else if (header.has(CAROUSEL_HEADER_RENDERER)) {
return header.getObject(CAROUSEL_HEADER_RENDERER)
.getArray(CONTENTS)
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(item -> item.has("topicChannelDetailsRenderer"))
.filter(item -> item.has(TOPIC_CHANNEL_DETAILS_RENDERER))
.findFirst()
.map(item -> item.getObject("topicChannelDetailsRenderer"))
.map(item -> item.getObject(TOPIC_CHANNEL_DETAILS_RENDERER))
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.CAROUSEL));
} else if (header.has("pageHeaderRenderer")) {
return Optional.of(header.getObject("pageHeaderRenderer"))
@ -320,4 +336,221 @@ public final class YoutubeChannelHelper {
return Optional.empty();
}
}
/**
* Check if a channel is verified by using its header.
*
* <p>
* The header is mandatory, so the verified status of age-restricted channels with a
* {@code channelAgeGateRenderer} cannot be checked.
* </p>
*
* @param channelHeader the {@link ChannelHeader} of a non age-restricted channel
* @return whether the channel is verified
*/
public static boolean isChannelVerified(@Nonnull final ChannelHeader channelHeader) {
switch (channelHeader.headerType) {
// carouselHeaderRenderers do not contain any verification badges
// Since they are only shown on YouTube internal channels or on channels of large
// organizations broadcasting live events, we can assume the channel to be verified
case CAROUSEL:
return true;
case PAGE:
final JsonObject pageHeaderViewModel = channelHeader.json.getObject(CONTENT)
.getObject(PAGE_HEADER_VIEW_MODEL);
final boolean hasCircleOrMusicIcon = pageHeaderViewModel.getObject(TITLE)
.getObject("dynamicTextViewModel")
.getObject("text")
.getArray("attachmentRuns")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.anyMatch(attachmentRun -> attachmentRun.getObject("element")
.getObject("type")
.getObject("imageType")
.getObject("image")
.getArray("sources")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.anyMatch(source -> {
final String imageName = source.getObject("clientResource")
.getString("imageName");
return "CHECK_CIRCLE_FILLED".equals(imageName)
|| "MUSIC_FILLED".equals(imageName);
}));
if (!hasCircleOrMusicIcon && pageHeaderViewModel.getObject("image")
.has("contentPreviewImageViewModel")) {
// If a pageHeaderRenderer has no object in which a check verified may be
// contained and if it has a contentPreviewImageViewModel, it should mean
// that the header is coming from a system channel, which we can assume to
// be verified
return true;
}
return hasCircleOrMusicIcon;
case INTERACTIVE_TABBED:
// If the header has an autoGenerated property, it should mean that the channel has
// been auto generated by YouTube: we can assume the channel to be verified in this
// case
return channelHeader.json.has("autoGenerated");
default:
return YoutubeParsingHelper.isVerified(channelHeader.json.getArray("badges"));
}
}
/**
* Get the ID of a channel from its response.
*
* <p>
* For {@link ChannelHeader.HeaderType#C4_TABBED c4TabbedHeaderRenderer} and
* {@link ChannelHeader.HeaderType#CAROUSEL carouselHeaderRenderer} channel headers, the ID is
* get from the header.
* </p>
*
* <p>
* For other headers or if it cannot be got, the ID from the {@code channelMetadataRenderer}
* in the channel response is used.
* </p>
*
* <p>
* If the ID cannot still be get, the fallback channel ID, if provided, will be used.
* </p>
*
* @param header the channel header
* @param fallbackChannelId the fallback channel ID, which can be null
* @return the ID of the channel
* @throws ParsingException if the channel ID cannot be got from the channel header, the
* channel response and the fallback channel ID
*/
@Nonnull
public static String getChannelId(
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Nonnull final Optional<ChannelHeader> header,
@Nonnull final JsonObject jsonResponse,
@Nullable final String fallbackChannelId) throws ParsingException {
if (header.isPresent()) {
final ChannelHeader channelHeader = header.get();
switch (channelHeader.headerType) {
case C4_TABBED:
final String channelId = channelHeader.json.getObject(HEADER)
.getObject(C4_TABBED_HEADER_RENDERER)
.getString("channelId", "");
if (!isNullOrEmpty(channelId)) {
return channelId;
}
final String navigationC4TabChannelId = channelHeader.json
.getObject("navigationEndpoint")
.getObject(BROWSE_ENDPOINT)
.getString(BROWSE_ID);
if (!isNullOrEmpty(navigationC4TabChannelId)) {
return navigationC4TabChannelId;
}
break;
case CAROUSEL:
final String navigationCarouselChannelId = channelHeader.json.getObject(HEADER)
.getObject(CAROUSEL_HEADER_RENDERER)
.getArray(CONTENTS)
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(item -> item.has(TOPIC_CHANNEL_DETAILS_RENDERER))
.findFirst()
.orElse(new JsonObject())
.getObject(TOPIC_CHANNEL_DETAILS_RENDERER)
.getObject("navigationEndpoint")
.getObject(BROWSE_ENDPOINT)
.getString(BROWSE_ID);
if (!isNullOrEmpty(navigationCarouselChannelId)) {
return navigationCarouselChannelId;
}
break;
default:
break;
}
}
final String externalChannelId = jsonResponse.getObject("metadata")
.getObject("channelMetadataRenderer")
.getString("externalChannelId");
if (!isNullOrEmpty(externalChannelId)) {
return externalChannelId;
}
if (!isNullOrEmpty(fallbackChannelId)) {
return fallbackChannelId;
} else {
throw new ParsingException("Could not get channel ID");
}
}
@Nonnull
public static String getChannelName(@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Nonnull final Optional<ChannelHeader> channelHeader,
@Nonnull final JsonObject jsonResponse,
@Nullable final JsonObject channelAgeGateRenderer)
throws ParsingException {
if (channelAgeGateRenderer != null) {
final String title = channelAgeGateRenderer.getString("channelTitle");
if (isNullOrEmpty(title)) {
throw new ParsingException("Could not get channel name");
}
return title;
}
final String metadataRendererTitle = jsonResponse.getObject("metadata")
.getObject("channelMetadataRenderer")
.getString(TITLE);
if (!isNullOrEmpty(metadataRendererTitle)) {
return metadataRendererTitle;
}
return channelHeader.map(header -> {
final JsonObject channelJson = header.json;
switch (header.headerType) {
case PAGE:
return channelJson.getObject(CONTENT)
.getObject(PAGE_HEADER_VIEW_MODEL)
.getObject(TITLE)
.getObject("dynamicTextViewModel")
.getObject("text")
.getString(CONTENT, channelJson.getString("pageTitle"));
case CAROUSEL:
case INTERACTIVE_TABBED:
return getTextFromObject(channelJson.getObject(TITLE));
case C4_TABBED:
default:
return channelJson.getString(TITLE);
}
})
// The channel name from a microformatDataRenderer may be different from the one
// displayed, especially for auto-generated channels, depending on the language
// requested for the interface (hl parameter of InnerTube requests' payload)
.or(() -> Optional.ofNullable(jsonResponse.getObject("microformat")
.getObject("microformatDataRenderer")
.getString(TITLE)))
.orElseThrow(() -> new ParsingException("Could not get channel name"));
}
@Nullable
public static JsonObject getChannelAgeGateRenderer(@Nonnull final JsonObject jsonResponse) {
return jsonResponse.getObject(CONTENTS)
.getObject("twoColumnBrowseResultsRenderer")
.getArray("tabs")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.flatMap(tab -> tab.getObject(TAB_RENDERER)
.getObject(CONTENT)
.getObject("sectionListRenderer")
.getArray(CONTENTS)
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast))
.filter(content -> content.has("channelAgeGateRenderer"))
.map(content -> content.getObject("channelAgeGateRenderer"))
.findFirst()
.orElse(null);
}
}

View File

@ -52,7 +52,6 @@ import org.schabi.newpipe.extractor.utils.RandomStringFromAlphabetGenerator;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@ -153,7 +152,7 @@ public final class YoutubeParsingHelper {
* The client version for InnerTube requests with the {@code WEB} client, used as the last
* fallback if the extraction of the real one failed.
*/
private static final String HARDCODED_CLIENT_VERSION = "2.20240410.01.00";
private static final String HARDCODED_CLIENT_VERSION = "2.20240718.01.00";
/**
* The hardcoded client version of the Android app used for InnerTube requests with this
@ -164,7 +163,7 @@ public final class YoutubeParsingHelper {
* such as <a href="https://www.apkmirror.com/apk/google-inc/youtube/">APKMirror</a>.
* </p>
*/
private static final String ANDROID_YOUTUBE_CLIENT_VERSION = "19.13.36";
private static final String ANDROID_YOUTUBE_CLIENT_VERSION = "19.28.35";
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
@ -175,7 +174,7 @@ public final class YoutubeParsingHelper {
* Store page of the YouTube app</a>, in the {@code Whats New} section.
* </p>
*/
private static final String IOS_YOUTUBE_CLIENT_VERSION = "19.14.3";
private static final String IOS_YOUTUBE_CLIENT_VERSION = "19.28.1";
/**
* The hardcoded client version used for InnerTube requests with the TV HTML5 embed client.
@ -191,7 +190,7 @@ public final class YoutubeParsingHelper {
* The hardcoded client version used for InnerTube requests with the YouTube Music desktop
* client.
*/
private static final String HARDCODED_YOUTUBE_MUSIC_CLIENT_VERSION = "1.20240403.01.00";
private static final String HARDCODED_YOUTUBE_MUSIC_CLIENT_VERSION = "1.20240715.01.00";
private static String clientVersion;
@ -220,31 +219,31 @@ public final class YoutubeParsingHelper {
* information.
* </p>
*/
private static final String IOS_DEVICE_MODEL = "iPhone15,4";
private static final String IOS_DEVICE_MODEL = "iPhone16,2";
/**
* Spoofing an iPhone 15 running iOS 17.4.1 with the hardcoded version of the iOS app. To be
* used for the {@code "osVersion"} field in JSON POST requests.
* Spoofing an iPhone 15 Pro Max running iOS 17.5.1 with the hardcoded version of the iOS app.
* To be used for the {@code "osVersion"} field in JSON POST requests.
* <p>
* The value of this field seems to use the following structure:
* "iOS major version.minor version.patch version.build version", where
* "patch version" is equal to 0 if it isn't set
* The build version corresponding to the iOS version used can be found on
* <a href="https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15">
* https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15</a>
* <a href="https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15_Pro_Max">
* https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15_Pro_Max</a>
* </p>
*
* @see #IOS_USER_AGENT_VERSION
*/
private static final String IOS_OS_VERSION = "17.4.1.21E237";
private static final String IOS_OS_VERSION = "17.5.1.21F90";
/**
* Spoofing an iPhone 15 running iOS 17.4.1 with the hardcoded version of the iOS app. To be
* Spoofing an iPhone 15 running iOS 17.5.1 with the hardcoded version of the iOS app. To be
* used in the user agent for requests.
*
* @see #IOS_OS_VERSION
*/
private static final String IOS_USER_AGENT_VERSION = "17_4_1";
private static final String IOS_USER_AGENT_VERSION = "17_5_1";
private static Random numberGenerator = new Random();
@ -810,11 +809,7 @@ public final class YoutubeParsingHelper {
final String[] params = internUrl.split("&");
for (final String param : params) {
if (param.split("=")[0].equals("q")) {
try {
return Utils.decodeUrlUtf8(param.split("=")[1]);
} catch (final UnsupportedEncodingException e) {
return null;
}
return Utils.decodeUrlUtf8(param.split("=")[1]);
}
}
} else if (internUrl.startsWith("http")) {
@ -830,9 +825,15 @@ public final class YoutubeParsingHelper {
final String canonicalBaseUrl = browseEndpoint.getString("canonicalBaseUrl");
final String browseId = browseEndpoint.getString("browseId");
// All channel ids are prefixed with UC
if (browseId != null && browseId.startsWith("UC")) {
return "https://www.youtube.com/channel/" + browseId;
if (browseId != null) {
if (browseId.startsWith("UC")) {
// All channel IDs are prefixed with UC
return "https://www.youtube.com/channel/" + browseId;
} else if (browseId.startsWith("VL")) {
// All playlist IDs are prefixed with VL, which needs to be removed from the
// playlist ID
return "https://www.youtube.com/playlist?list=" + browseId.substring(2);
}
}
if (!isNullOrEmpty(canonicalBaseUrl)) {
@ -892,12 +893,13 @@ public final class YoutubeParsingHelper {
return textObject.getString("simpleText");
}
if (textObject.getArray("runs").isEmpty()) {
final JsonArray runs = textObject.getArray("runs");
if (runs.isEmpty()) {
return null;
}
final StringBuilder textBuilder = new StringBuilder();
for (final Object o : textObject.getArray("runs")) {
for (final Object o : runs) {
final JsonObject run = (JsonObject) o;
String text = run.getString("text");
@ -975,11 +977,12 @@ public final class YoutubeParsingHelper {
return null;
}
if (textObject.getArray("runs").isEmpty()) {
final JsonArray runs = textObject.getArray("runs");
if (runs.isEmpty()) {
return null;
}
for (final Object textPart : textObject.getArray("runs")) {
for (final Object textPart : runs) {
final String url = getUrlFromNavigationEndpoint(((JsonObject) textPart)
.getObject("navigationEndpoint"));
if (!isNullOrEmpty(url)) {
@ -1306,31 +1309,49 @@ public final class YoutubeParsingHelper {
}
@Nonnull
public static byte[] createDesktopPlayerBody(
public static JsonObject getWebPlayerResponse(
@Nonnull final Localization localization,
@Nonnull final ContentCountry contentCountry,
@Nonnull final String videoId) throws IOException, ExtractionException {
final byte[] body = JsonWriter.string(
prepareDesktopJsonBuilder(localization, contentCountry)
.value(VIDEO_ID, videoId)
.value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true)
.done())
.getBytes(StandardCharsets.UTF_8);
final String url = YOUTUBEI_V1_URL + "player" + "?" + DISABLE_PRETTY_PRINT_PARAMETER
+ "&$fields=microformat,playabilityStatus,storyboards,videoDetails";
return JsonUtils.toJsonObject(getValidJsonResponseBody(
getDownloader().postWithContentTypeJson(
url, getYouTubeHeaders(), body, localization)));
}
@Nonnull
public static byte[] createTvHtml5EmbedPlayerBody(
@Nonnull final Localization localization,
@Nonnull final ContentCountry contentCountry,
@Nonnull final String videoId,
@Nonnull final Integer sts,
final boolean isTvHtml5DesktopJsonBuilder,
@Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException {
@Nonnull final String contentPlaybackNonce) {
// @formatter:off
return JsonWriter.string((isTvHtml5DesktopJsonBuilder
? prepareTvHtml5EmbedJsonBuilder(localization, contentCountry, videoId)
: prepareDesktopJsonBuilder(localization, contentCountry))
.object("playbackContext")
.object("contentPlaybackContext")
// Signature timestamp from the JavaScript base player is needed to get
// working obfuscated URLs
.value("signatureTimestamp", sts)
.value("referer", "https://www.youtube.com/watch?v=" + videoId)
return JsonWriter.string(
prepareTvHtml5EmbedJsonBuilder(localization, contentCountry, videoId)
.object("playbackContext")
.object("contentPlaybackContext")
// Signature timestamp from the JavaScript base player is needed to get
// working obfuscated URLs
.value("signatureTimestamp", sts)
.value("referer", "https://www.youtube.com/watch?v=" + videoId)
.end()
.end()
.end()
.value(CPN, contentPlaybackNonce)
.value(VIDEO_ID, videoId)
.value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true)
.done())
.getBytes(StandardCharsets.UTF_8);
.value(CPN, contentPlaybackNonce)
.value(VIDEO_ID, videoId)
.value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true)
.done())
.getBytes(StandardCharsets.UTF_8);
// @formatter:on
}
@ -1371,7 +1392,7 @@ public final class YoutubeParsingHelper {
*/
@Nonnull
public static String getIosUserAgent(@Nullable final Localization localization) {
// Spoofing an iPhone 15 running iOS 17.4.1 with the hardcoded version of the iOS app
// Spoofing an iPhone 15 running iOS 17.5.1 with the hardcoded version of the iOS app
return "com.google.ios.youtube/" + IOS_YOUTUBE_CLIENT_VERSION
+ "(" + IOS_DEVICE_MODEL + "; U; CPU iOS "
+ IOS_USER_AGENT_VERSION + " like Mac OS X; "

View File

@ -26,24 +26,54 @@ final class YoutubeThrottlingParameterUtils {
private static final String ARRAY_ACCESS_REGEX = "\\[(\\d+)]";
/**
* The first regex matches this, where we want BDa:
* <p>
* (b=String.fromCharCode(110),c=a.get(b))&&(c=<strong>BDa</strong><strong>[0]</strong>(c)
* <p>
* Array access is optional, but needs to be handled, since the actual function is inside the
* array.
*/
// CHECKSTYLE:OFF
private static final Pattern[] DEOBFUSCATION_FUNCTION_NAME_REGEXES = {
/*
* The first regex matches the following text, where we want rDa and the array index
* accessed:
*
* a.D&&(b="nn"[+a.D],c=a.get(b))&&(c=rDa[0](c),a.set(b,c),rDa.length||rma("")
*/
Pattern.compile(SINGLE_CHAR_VARIABLE_REGEX + "+=\"nn\"\\[\\+"
+ SINGLE_CHAR_VARIABLE_REGEX + "+\\." + SINGLE_CHAR_VARIABLE_REGEX + "+],"
+ SINGLE_CHAR_VARIABLE_REGEX + "+=" + SINGLE_CHAR_VARIABLE_REGEX
+ "+\\.get\\(" + SINGLE_CHAR_VARIABLE_REGEX + "+\\)\\)&&\\("
+ SINGLE_CHAR_VARIABLE_REGEX + "+=(" + SINGLE_CHAR_VARIABLE_REGEX
+ "+)\\[(\\d+)]"),
/*
* The second regex matches the following text, where we want rma:
*
* a.D&&(b="nn"[+a.D],c=a.get(b))&&(c=rDa[0](c),a.set(b,c),rDa.length||rma("")
*/
Pattern.compile(SINGLE_CHAR_VARIABLE_REGEX + "+=\"nn\"\\[\\+"
+ SINGLE_CHAR_VARIABLE_REGEX + "+\\." + SINGLE_CHAR_VARIABLE_REGEX + "+],"
+ SINGLE_CHAR_VARIABLE_REGEX + "+=" + SINGLE_CHAR_VARIABLE_REGEX + "+\\.get\\("
+ SINGLE_CHAR_VARIABLE_REGEX + "+\\)\\).+\\|\\|(" + SINGLE_CHAR_VARIABLE_REGEX
+ "+)\\(\"\"\\)"),
/*
* The third regex matches the following text, where we want BDa and the array index
* accessed:
*
* (b=String.fromCharCode(110),c=a.get(b))&&(c=BDa[0](c)
*/
Pattern.compile("\\(" + SINGLE_CHAR_VARIABLE_REGEX + "=String\\.fromCharCode\\(110\\),"
+ SINGLE_CHAR_VARIABLE_REGEX + "=" + SINGLE_CHAR_VARIABLE_REGEX + "\\.get\\("
+ SINGLE_CHAR_VARIABLE_REGEX + "\\)\\)" + "&&\\(" + SINGLE_CHAR_VARIABLE_REGEX
+ "=(" + FUNCTION_NAME_REGEX + ")" + "(?:" + ARRAY_ACCESS_REGEX + ")?\\("
+ SINGLE_CHAR_VARIABLE_REGEX + "\\)"),
/*
* The fourth regex matches the following text, where we want Yva and the array index
* accessed:
*
* .get("n"))&&(b=Yva[0](b)
*/
Pattern.compile("\\.get\\(\"n\"\\)\\)&&\\(" + SINGLE_CHAR_VARIABLE_REGEX
+ "=(" + FUNCTION_NAME_REGEX + ")(?:" + ARRAY_ACCESS_REGEX + ")?\\("
+ SINGLE_CHAR_VARIABLE_REGEX + "\\)"),
+ SINGLE_CHAR_VARIABLE_REGEX + "\\)")
};
// CHECKSTYLE:ON

View File

@ -0,0 +1,65 @@
package org.schabi.newpipe.extractor.services.youtube.extractors;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
import org.schabi.newpipe.extractor.utils.Utils;
import javax.annotation.Nonnull;
import java.util.List;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getThumbnailsFromInfoItem;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
/**
* The base {@link PlaylistInfoItemExtractor} for shows playlists UI elements.
*/
abstract class YoutubeBaseShowInfoItemExtractor implements PlaylistInfoItemExtractor {
@Nonnull
protected final JsonObject showRenderer;
YoutubeBaseShowInfoItemExtractor(@Nonnull final JsonObject showRenderer) {
this.showRenderer = showRenderer;
}
@Override
public String getName() throws ParsingException {
return showRenderer.getString("title");
}
@Override
public String getUrl() throws ParsingException {
return getUrlFromNavigationEndpoint(showRenderer.getObject("navigationEndpoint"));
}
@Nonnull
@Override
public List<Image> getThumbnails() throws ParsingException {
return getThumbnailsFromInfoItem(showRenderer.getObject("thumbnailRenderer")
.getObject("showCustomThumbnailRenderer"));
}
@Override
public long getStreamCount() throws ParsingException {
// The stream count should be always returned in the first text object for English
// localizations, but the complete text is parsed for reliability purposes
final String streamCountText = getTextFromObject(
showRenderer.getObject("thumbnailOverlays")
.getObject("thumbnailOverlayBottomPanelRenderer")
.getObject("text"));
if (streamCountText == null) {
throw new ParsingException("Could not get stream count");
}
try {
// The data returned could be a human/shortened number, but no show with more than 1000
// videos has been found at the time this code was written
return Long.parseLong(Utils.removeNonDigitCharacters(streamCountText));
} catch (final NumberFormatException e) {
throw new ParsingException("Could not convert stream count to a long", e);
}
}
}

View File

@ -23,7 +23,6 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper.getChannelResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper.resolveChannelId;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
@ -59,6 +58,19 @@ import javax.annotation.Nullable;
public class YoutubeChannelExtractor extends ChannelExtractor {
// Constants of objects used multiples from channel responses
private static final String IMAGE = "image";
private static final String CONTENTS = "contents";
private static final String CONTENT_PREVIEW_IMAGE_VIEW_MODEL = "contentPreviewImageViewModel";
private static final String PAGE_HEADER_VIEW_MODEL = "pageHeaderViewModel";
private static final String TAB_RENDERER = "tabRenderer";
private static final String CONTENT = "content";
private static final String METADATA = "metadata";
private static final String AVATAR = "avatar";
private static final String THUMBNAILS = "thumbnails";
private static final String SOURCES = "sources";
private static final String BANNER = "banner";
private JsonObject jsonResponse;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@ -95,28 +107,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
jsonResponse = data.jsonResponse;
channelHeader = YoutubeChannelHelper.getChannelHeader(jsonResponse);
channelId = data.channelId;
channelAgeGateRenderer = getChannelAgeGateRenderer();
}
@Nullable
private JsonObject getChannelAgeGateRenderer() {
return jsonResponse.getObject("contents")
.getObject("twoColumnBrowseResultsRenderer")
.getArray("tabs")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.flatMap(tab -> tab.getObject("tabRenderer")
.getObject("content")
.getObject("sectionListRenderer")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast))
.filter(content -> content.has("channelAgeGateRenderer"))
.map(content -> content.getObject("channelAgeGateRenderer"))
.findFirst()
.orElse(null);
channelAgeGateRenderer = YoutubeChannelHelper.getChannelAgeGateRenderer(jsonResponse);
}
@Nonnull
@ -133,62 +124,15 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
@Override
public String getId() throws ParsingException {
assertPageFetched();
return channelHeader.map(header -> header.json)
.flatMap(header -> Optional.ofNullable(header.getString("channelId"))
.or(() -> Optional.ofNullable(header.getObject("navigationEndpoint")
.getObject("browseEndpoint")
.getString("browseId"))
))
.or(() -> Optional.ofNullable(channelId))
.orElseThrow(() -> new ParsingException("Could not get channel ID"));
return YoutubeChannelHelper.getChannelId(channelHeader, jsonResponse, channelId);
}
@Nonnull
@Override
public String getName() throws ParsingException {
assertPageFetched();
if (channelAgeGateRenderer != null) {
final String title = channelAgeGateRenderer.getString("channelTitle");
if (isNullOrEmpty(title)) {
throw new ParsingException("Could not get channel name");
}
return title;
}
final String metadataRendererTitle = jsonResponse.getObject("metadata")
.getObject("channelMetadataRenderer")
.getString("title");
if (!isNullOrEmpty(metadataRendererTitle)) {
return metadataRendererTitle;
}
return channelHeader.map(header -> {
final JsonObject channelJson = header.json;
switch (header.headerType) {
case PAGE:
return channelJson.getObject("content")
.getObject("pageHeaderViewModel")
.getObject("title")
.getObject("dynamicTextViewModel")
.getObject("text")
.getString("content", channelJson.getString("pageTitle"));
case CAROUSEL:
case INTERACTIVE_TABBED:
return getTextFromObject(channelJson.getObject("title"));
case C4_TABBED:
default:
return channelJson.getString("title");
}
})
// The channel name from a microformatDataRenderer may be different from the one displayed,
// especially for auto-generated channels, depending on the language requested for the
// interface (hl parameter of InnerTube requests' payload)
.or(() -> Optional.ofNullable(jsonResponse.getObject("microformat")
.getObject("microformatDataRenderer")
.getString("title")))
.orElseThrow(() -> new ParsingException("Could not get channel name"));
return YoutubeChannelHelper.getChannelName(
channelHeader, jsonResponse, channelAgeGateRenderer);
}
@Nonnull
@ -196,8 +140,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
public List<Image> getAvatars() throws ParsingException {
assertPageFetched();
if (channelAgeGateRenderer != null) {
return Optional.ofNullable(channelAgeGateRenderer.getObject("avatar")
.getArray("thumbnails"))
return Optional.ofNullable(channelAgeGateRenderer.getObject(AVATAR)
.getArray(THUMBNAILS))
.map(YoutubeParsingHelper::getImagesFromThumbnailsArray)
.orElseThrow(() -> new ParsingException("Could not get avatars"));
}
@ -205,22 +149,35 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
return channelHeader.map(header -> {
switch (header.headerType) {
case PAGE:
return header.json.getObject("content")
.getObject("pageHeaderViewModel")
.getObject("image")
.getObject("contentPreviewImageViewModel")
.getObject("image")
.getArray("sources");
final JsonObject imageObj = header.json.getObject(CONTENT)
.getObject(PAGE_HEADER_VIEW_MODEL)
.getObject(IMAGE);
if (imageObj.has(CONTENT_PREVIEW_IMAGE_VIEW_MODEL)) {
return imageObj.getObject(CONTENT_PREVIEW_IMAGE_VIEW_MODEL)
.getObject(IMAGE)
.getArray(SOURCES);
}
if (imageObj.has("decoratedAvatarViewModel")) {
return imageObj.getObject("decoratedAvatarViewModel")
.getObject(AVATAR)
.getObject("avatarViewModel")
.getObject(IMAGE)
.getArray(SOURCES);
}
// Return an empty avatar array as a fallback
return new JsonArray();
case INTERACTIVE_TABBED:
return header.json.getObject("boxArt")
.getArray("thumbnails");
.getArray(THUMBNAILS);
case C4_TABBED:
case CAROUSEL:
default:
return header.json.getObject("avatar")
.getArray("thumbnails");
return header.json.getObject(AVATAR)
.getArray(THUMBNAILS);
}
})
.map(YoutubeParsingHelper::getImagesFromThumbnailsArray)
@ -235,10 +192,27 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
return List.of();
}
// No banner is available on pageHeaderRenderer headers
return channelHeader.filter(header -> header.headerType != HeaderType.PAGE)
.map(header -> header.json.getObject("banner")
.getArray("thumbnails"))
return channelHeader.map(header -> {
if (header.headerType == HeaderType.PAGE) {
final JsonObject pageHeaderViewModel = header.json.getObject(CONTENT)
.getObject(PAGE_HEADER_VIEW_MODEL);
if (pageHeaderViewModel.has(BANNER)) {
return pageHeaderViewModel.getObject(BANNER)
.getObject("imageBannerViewModel")
.getObject(IMAGE)
.getArray(SOURCES);
}
// No banner is available (this should happen on pageHeaderRenderers of
// system channels), use an empty JsonArray instead
return new JsonArray();
}
return header.json
.getObject(BANNER)
.getArray(THUMBNAILS);
})
.map(YoutubeParsingHelper::getImagesFromThumbnailsArray)
.orElse(List.of());
}
@ -264,14 +238,16 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
if (channelHeader.isPresent()) {
final ChannelHeader header = channelHeader.get();
if (header.headerType == HeaderType.INTERACTIVE_TABBED
|| header.headerType == HeaderType.PAGE) {
// No subscriber count is available on interactiveTabbedHeaderRenderer and
// pageHeaderRenderer headers
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
// No subscriber count is available on interactiveTabbedHeaderRenderer header
return UNKNOWN_SUBSCRIBER_COUNT;
}
final JsonObject headerJson = header.json;
if (header.headerType == HeaderType.PAGE) {
return getSubscriberCountFromPageChannelHeader(headerJson);
}
JsonObject textObject = null;
if (headerJson.has("subscriberCountText")) {
@ -292,6 +268,51 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
return UNKNOWN_SUBSCRIBER_COUNT;
}
private long getSubscriberCountFromPageChannelHeader(@Nonnull final JsonObject headerJson)
throws ParsingException {
final JsonObject metadataObject = headerJson.getObject(CONTENT)
.getObject(PAGE_HEADER_VIEW_MODEL)
.getObject(METADATA);
if (metadataObject.has("contentMetadataViewModel")) {
final JsonArray metadataPart = metadataObject.getObject("contentMetadataViewModel")
.getArray("metadataRows")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(metadataRow -> metadataRow.getArray("metadataParts"))
/*
Find metadata parts which have two elements: channel handle and subscriber
count.
On autogenerated music channels, the subscriber count is not shown with this
header.
Use the first metadata parts object found.
*/
.filter(metadataParts -> metadataParts.size() == 2)
.findFirst()
.orElse(null);
if (metadataPart == null) {
// As the parsing of the metadata parts object needed to get the subscriber count
// is fragile, return UNKNOWN_SUBSCRIBER_COUNT when it cannot be got
return UNKNOWN_SUBSCRIBER_COUNT;
}
try {
// The subscriber count is at the same position for all languages as of 02/03/2024
return Utils.mixedNumberWordToLong(metadataPart.getObject(0)
.getObject("text")
.getString(CONTENT));
} catch (final NumberFormatException e) {
throw new ParsingException("Could not get subscriber count", e);
}
}
// If the channel header has no contentMetadataViewModel (which is the case for system
// channels using this header), return UNKNOWN_SUBSCRIBER_COUNT
return UNKNOWN_SUBSCRIBER_COUNT;
}
@Override
public String getDescription() throws ParsingException {
assertPageFetched();
@ -302,12 +323,6 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
try {
if (channelHeader.isPresent()) {
final ChannelHeader header = channelHeader.get();
if (header.headerType == HeaderType.PAGE) {
// A pageHeaderRenderer doesn't contain a description
return null;
}
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
/*
In an interactiveTabbedHeaderRenderer, the real description, is only available
@ -321,8 +336,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
}
}
// The description is cut and the original one can be only accessed from the About tab
return jsonResponse.getObject("metadata")
return jsonResponse.getObject(METADATA)
.getObject("channelMetadataRenderer")
.getString("description");
} catch (final Exception e) {
@ -350,31 +364,12 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
public boolean isVerified() throws ParsingException {
assertPageFetched();
if (channelAgeGateRenderer != null) {
// Verified status is unknown with channelAgeGateRenderers, return false in this case
return false;
}
if (channelHeader.isPresent()) {
final ChannelHeader header = channelHeader.get();
// carouselHeaderRenderer and pageHeaderRenderer does not contain any verification
// badges
// Since they are only shown on YouTube internal channels or on channels of large
// organizations broadcasting live events, we can assume the channel to be verified
if (header.headerType == HeaderType.CAROUSEL || header.headerType == HeaderType.PAGE) {
return true;
}
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
// If the header has an autoGenerated property, it should mean that the channel has
// been auto generated by YouTube: we can assume the channel to be verified in this
// case
return header.json.has("autoGenerated");
}
return YoutubeParsingHelper.isVerified(header.json.getArray("badges"));
}
return false;
return YoutubeChannelHelper.isChannelVerified(channelHeader.orElseThrow(() ->
new ParsingException("Could not get verified status")));
}
@Nonnull
@ -390,7 +385,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
@Nonnull
private List<ListLinkHandler> getTabsForNonAgeRestrictedChannels() throws ParsingException {
final JsonArray responseTabs = jsonResponse.getObject("contents")
final JsonArray responseTabs = jsonResponse.getObject(CONTENTS)
.getObject("twoColumnBrowseResultsRenderer")
.getArray("tabs");
@ -411,8 +406,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
responseTabs.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(tab -> tab.has("tabRenderer"))
.map(tab -> tab.getObject("tabRenderer"))
.filter(tab -> tab.has(TAB_RENDERER))
.map(tab -> tab.getObject(TAB_RENDERER))
.forEach(tabRenderer -> {
final String tabUrl = tabRenderer.getObject("endpoint")
.getObject("commandMetadata")
@ -436,7 +431,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
channelId,
ChannelTabs.VIDEOS,
(service, linkHandler) -> new VideosTabExtractor(
service, linkHandler, tabRenderer, name, id, url)));
service, linkHandler, tabRenderer, channelHeader,
name, id, url)));
break;
case "shorts":
@ -451,6 +447,9 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
case "playlists":
addNonVideosTab.accept(ChannelTabs.PLAYLISTS);
break;
default:
// Unsupported channel tab, ignore it
break;
}
}
});

View File

@ -29,7 +29,6 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper
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.getJsonPostResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@ -37,8 +36,8 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
* A {@link ChannelTabExtractor} implementation for the YouTube service.
*
* <p>
* It currently supports {@code Videos}, {@code Shorts}, {@code Live}, {@code Playlists} and
* {@code Channels} tabs.
* It currently supports {@code Videos}, {@code Shorts}, {@code Live}, {@code Playlists},
* {@code Albums} and {@code Channels} tabs.
* </p>
*/
public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@ -60,6 +59,8 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
private String channelId;
@Nullable
private String visitorData;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
protected Optional<YoutubeChannelHelper.ChannelHeader> channelHeader;
public YoutubeChannelTabExtractor(final StreamingService service,
final ListLinkHandler linkHandler) {
@ -89,14 +90,15 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@Override
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
ExtractionException {
channelId = resolveChannelId(super.getId());
final String channelIdFromId = resolveChannelId(super.getId());
final String params = getChannelTabsParameters();
final YoutubeChannelHelper.ChannelResponseData data = getChannelResponse(channelId,
final YoutubeChannelHelper.ChannelResponseData data = getChannelResponse(channelIdFromId,
params, getExtractorLocalization(), getExtractorContentCountry());
jsonResponse = data.jsonResponse;
channelHeader = YoutubeChannelHelper.getChannelHeader(jsonResponse);
channelId = data.channelId;
if (useVisitorData) {
visitorData = jsonResponse.getObject("responseContext").getString("visitorData");
@ -117,60 +119,13 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@Nonnull
@Override
public String getId() throws ParsingException {
final String id = jsonResponse.getObject("header")
.getObject("c4TabbedHeaderRenderer")
.getString("channelId", "");
if (!id.isEmpty()) {
return id;
}
final Optional<String> carouselHeaderId = jsonResponse.getObject("header")
.getObject("carouselHeaderRenderer")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(item -> item.has("topicChannelDetailsRenderer"))
.findFirst()
.flatMap(item ->
Optional.ofNullable(item.getObject("topicChannelDetailsRenderer")
.getObject("navigationEndpoint")
.getObject("browseEndpoint")
.getString("browseId")));
if (carouselHeaderId.isPresent()) {
return carouselHeaderId.get();
}
if (!isNullOrEmpty(channelId)) {
return channelId;
} else {
throw new ParsingException("Could not get channel ID");
}
return YoutubeChannelHelper.getChannelId(channelHeader, jsonResponse, channelId);
}
protected String getChannelName() {
final String metadataName = jsonResponse.getObject("metadata")
.getObject("channelMetadataRenderer")
.getString("title");
if (!isNullOrEmpty(metadataName)) {
return metadataName;
}
return YoutubeChannelHelper.getChannelHeader(jsonResponse)
.map(header -> {
final Object title = header.json.get("title");
if (title instanceof String) {
return (String) title;
} else if (title instanceof JsonObject) {
final String headerName = getTextFromObject((JsonObject) title);
if (!isNullOrEmpty(headerName)) {
return headerName;
}
}
return "";
})
.orElse("");
protected String getChannelName() throws ParsingException {
return YoutubeChannelHelper.getChannelName(
channelHeader, jsonResponse,
YoutubeChannelHelper.getChannelAgeGateRenderer(jsonResponse));
}
@Nonnull
@ -204,18 +159,27 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
}
}
final VerifiedStatus verifiedStatus = channelHeader.flatMap(header ->
YoutubeChannelHelper.isChannelVerified(header)
? Optional.of(VerifiedStatus.VERIFIED)
: Optional.of(VerifiedStatus.UNVERIFIED))
.orElse(VerifiedStatus.UNKNOWN);
// If a channel tab is fetched, the next page requires channel ID and name, as channel
// streams don't have their channel specified.
// We also need to set the visitor data here when it should be enabled, as it is required
// to get continuations on some channel tabs, and we need a way to pass it between pages
final List<String> channelIds = useVisitorData && !isNullOrEmpty(visitorData)
? List.of(getChannelName(), getUrl(), visitorData)
: List.of(getChannelName(), getUrl());
final String channelName = getChannelName();
final String channelUrl = getUrl();
final JsonObject continuation = collectItemsFrom(collector, items, channelIds)
final JsonObject continuation = collectItemsFrom(collector, items, verifiedStatus,
channelName, channelUrl)
.orElse(null);
final Page nextPage = getNextPageFrom(continuation, channelIds);
final Page nextPage = getNextPageFrom(continuation,
useVisitorData && !isNullOrEmpty(visitorData)
? List.of(channelName, channelUrl, verifiedStatus.toString(), visitorData)
: List.of(channelName, channelUrl, verifiedStatus.toString()));
return new InfoItemsPage<>(collector, nextPage);
}
@ -281,16 +245,48 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
private Optional<JsonObject> collectItemsFrom(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final JsonArray items,
@Nonnull final List<String> channelIds) {
final String channelName;
final String channelUrl;
VerifiedStatus verifiedStatus;
if (channelIds.size() >= 3) {
channelName = channelIds.get(0);
channelUrl = channelIds.get(1);
try {
verifiedStatus = VerifiedStatus.valueOf(channelIds.get(2));
} catch (final IllegalArgumentException e) {
// An IllegalArgumentException can be thrown if someone passes a third channel ID
// which is not of the enum type in the getPage method, use the UNKNOWN
// VerifiedStatus enum value in this case
verifiedStatus = VerifiedStatus.UNKNOWN;
}
} else {
channelName = null;
channelUrl = null;
verifiedStatus = VerifiedStatus.UNKNOWN;
}
return collectItemsFrom(collector, items, verifiedStatus, channelName, channelUrl);
}
private Optional<JsonObject> collectItemsFrom(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final JsonArray items,
@Nonnull final VerifiedStatus verifiedStatus,
@Nullable final String channelName,
@Nullable final String channelUrl) {
return items.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(item -> collectItem(collector, item, channelIds))
.map(item -> collectItem(
collector, item, verifiedStatus, channelName, channelUrl))
.reduce(Optional.empty(), (c1, c2) -> c1.or(() -> c2));
}
private Optional<JsonObject> collectItem(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final JsonObject item,
@Nonnull final List<String> channelIds) {
@Nonnull final VerifiedStatus channelVerifiedStatus,
@Nullable final String channelName,
@Nullable final String channelUrl) {
final TimeAgoParser timeAgoParser = getTimeAgoParser();
if (item.has("richItemRenderer")) {
@ -298,33 +294,37 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
.getObject("content");
if (richItem.has("videoRenderer")) {
getCommitVideoConsumer(collector, timeAgoParser, channelIds,
richItem.getObject("videoRenderer"));
commitVideo(collector, timeAgoParser, richItem.getObject("videoRenderer"),
channelVerifiedStatus, channelName, channelUrl);
} else if (richItem.has("reelItemRenderer")) {
getCommitReelItemConsumer(collector, channelIds,
richItem.getObject("reelItemRenderer"));
commitReel(collector, richItem.getObject("reelItemRenderer"),
channelVerifiedStatus, channelName, channelUrl);
} else if (richItem.has("playlistRenderer")) {
getCommitPlaylistConsumer(collector, channelIds,
richItem.getObject("playlistRenderer"));
commitPlaylist(collector, richItem.getObject("playlistRenderer"),
channelVerifiedStatus, channelName, channelUrl);
}
} else if (item.has("gridVideoRenderer")) {
getCommitVideoConsumer(collector, timeAgoParser, channelIds,
item.getObject("gridVideoRenderer"));
commitVideo(collector, timeAgoParser, item.getObject("gridVideoRenderer"),
channelVerifiedStatus, channelName, channelUrl);
} else if (item.has("gridPlaylistRenderer")) {
getCommitPlaylistConsumer(collector, channelIds,
item.getObject("gridPlaylistRenderer"));
commitPlaylist(collector, item.getObject("gridPlaylistRenderer"),
channelVerifiedStatus, channelName, channelUrl);
} else if (item.has("gridShowRenderer")) {
collector.commit(new YoutubeGridShowRendererChannelInfoItemExtractor(
item.getObject("gridShowRenderer"), channelVerifiedStatus, channelName,
channelUrl));
} else if (item.has("shelfRenderer")) {
return collectItem(collector, item.getObject("shelfRenderer")
.getObject("content"), channelIds);
.getObject("content"), channelVerifiedStatus, channelName, channelUrl);
} else if (item.has("itemSectionRenderer")) {
return collectItemsFrom(collector, item.getObject("itemSectionRenderer")
.getArray("contents"), channelIds);
.getArray("contents"), channelVerifiedStatus, channelName, channelUrl);
} else if (item.has("horizontalListRenderer")) {
return collectItemsFrom(collector, item.getObject("horizontalListRenderer")
.getArray("items"), channelIds);
.getArray("items"), channelVerifiedStatus, channelName, channelUrl);
} else if (item.has("expandedShelfContentsRenderer")) {
return collectItemsFrom(collector, item.getObject("expandedShelfContentsRenderer")
.getArray("items"), channelIds);
.getArray("items"), channelVerifiedStatus, channelName, channelUrl);
} else if (item.has("continuationItemRenderer")) {
return Optional.ofNullable(item.getObject("continuationItemRenderer"));
}
@ -332,72 +332,91 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
return Optional.empty();
}
private void getCommitVideoConsumer(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final TimeAgoParser timeAgoParser,
@Nonnull final List<String> channelIds,
@Nonnull final JsonObject jsonObject) {
private static void commitReel(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final JsonObject reelItemRenderer,
@Nonnull final VerifiedStatus channelVerifiedStatus,
@Nullable final String channelName,
@Nullable final String channelUrl) {
collector.commit(
new YoutubeReelInfoItemExtractor(reelItemRenderer) {
@Override
public String getUploaderName() throws ParsingException {
return isNullOrEmpty(channelName) ? super.getUploaderName() : channelName;
}
@Override
public String getUploaderUrl() throws ParsingException {
return isNullOrEmpty(channelUrl) ? super.getUploaderName() : channelUrl;
}
@Override
public boolean isUploaderVerified() {
return channelVerifiedStatus == VerifiedStatus.VERIFIED;
}
});
}
private void commitVideo(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final TimeAgoParser timeAgoParser,
@Nonnull final JsonObject jsonObject,
@Nonnull final VerifiedStatus channelVerifiedStatus,
@Nullable final String channelName,
@Nullable final String channelUrl) {
collector.commit(
new YoutubeStreamInfoItemExtractor(jsonObject, timeAgoParser) {
@Override
public String getUploaderName() throws ParsingException {
if (channelIds.size() >= 2) {
return channelIds.get(0);
}
return super.getUploaderName();
return isNullOrEmpty(channelName) ? super.getUploaderName() : channelName;
}
@Override
public String getUploaderUrl() throws ParsingException {
if (channelIds.size() >= 2) {
return channelIds.get(1);
return isNullOrEmpty(channelUrl) ? super.getUploaderName() : channelUrl;
}
@SuppressWarnings("DuplicatedCode")
@Override
public boolean isUploaderVerified() throws ParsingException {
switch (channelVerifiedStatus) {
case VERIFIED:
return true;
case UNVERIFIED:
return false;
default:
return super.isUploaderVerified();
}
return super.getUploaderUrl();
}
});
}
private void getCommitReelItemConsumer(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final List<String> channelIds,
@Nonnull final JsonObject jsonObject) {
collector.commit(
new YoutubeReelInfoItemExtractor(jsonObject) {
@Override
public String getUploaderName() throws ParsingException {
if (channelIds.size() >= 2) {
return channelIds.get(0);
}
return super.getUploaderName();
}
@Override
public String getUploaderUrl() throws ParsingException {
if (channelIds.size() >= 2) {
return channelIds.get(1);
}
return super.getUploaderUrl();
}
});
}
private void getCommitPlaylistConsumer(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final List<String> channelIds,
@Nonnull final JsonObject jsonObject) {
private void commitPlaylist(@Nonnull final MultiInfoItemsCollector collector,
@Nonnull final JsonObject jsonObject,
@Nonnull final VerifiedStatus channelVerifiedStatus,
@Nullable final String channelName,
@Nullable final String channelUrl) {
collector.commit(
new YoutubePlaylistInfoItemExtractor(jsonObject) {
@Override
public String getUploaderName() throws ParsingException {
if (channelIds.size() >= 2) {
return channelIds.get(0);
}
return super.getUploaderName();
return isNullOrEmpty(channelName) ? super.getUploaderName() : channelName;
}
@Override
public String getUploaderUrl() throws ParsingException {
if (channelIds.size() >= 2) {
return channelIds.get(1);
return isNullOrEmpty(channelUrl) ? super.getUploaderName() : channelUrl;
}
@SuppressWarnings("DuplicatedCode")
@Override
public boolean isUploaderVerified() throws ParsingException {
switch (channelVerifiedStatus) {
case VERIFIED:
return true;
case UNVERIFIED:
return false;
default:
return super.isUploaderVerified();
}
return super.getUploaderUrl();
}
});
}
@ -431,20 +450,24 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
*/
public static final class VideosTabExtractor extends YoutubeChannelTabExtractor {
private final JsonObject tabRenderer;
private final String channelName;
private final String channelId;
private final String channelName;
private final String channelUrl;
VideosTabExtractor(final StreamingService service,
final ListLinkHandler linkHandler,
final JsonObject tabRenderer,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
final Optional<YoutubeChannelHelper.ChannelHeader> channelHeader,
final String channelName,
final String channelId,
final String channelUrl) {
super(service, linkHandler);
this.channelHeader = channelHeader;
this.tabRenderer = tabRenderer;
this.channelName = channelName;
this.channelId = channelId;
this.channelName = channelName;
this.channelUrl = channelUrl;
}
@ -475,4 +498,59 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
return Optional.of(tabRenderer);
}
}
/**
* Enum representing the verified state of a channel
*/
private enum VerifiedStatus {
VERIFIED,
UNVERIFIED,
UNKNOWN
}
private static final class YoutubeGridShowRendererChannelInfoItemExtractor
extends YoutubeBaseShowInfoItemExtractor {
@Nonnull
private final VerifiedStatus verifiedStatus;
@Nullable
private final String channelName;
@Nullable
private final String channelUrl;
private YoutubeGridShowRendererChannelInfoItemExtractor(
@Nonnull final JsonObject gridShowRenderer,
@Nonnull final VerifiedStatus verifiedStatus,
@Nullable final String channelName,
@Nullable final String channelUrl) {
super(gridShowRenderer);
this.verifiedStatus = verifiedStatus;
this.channelName = channelName;
this.channelUrl = channelUrl;
}
@Override
public String getUploaderName() {
return channelName;
}
@Override
public String getUploaderUrl() {
return channelUrl;
}
@Override
public boolean isUploaderVerified() throws ParsingException {
switch (verifiedStatus) {
case VERIFIED:
return true;
case UNVERIFIED:
return false;
default:
throw new ParsingException("Could not get uploader verification status");
}
}
}
}

View File

@ -238,9 +238,14 @@ public class YoutubeSearchExtractor extends SearchExtractor {
} else if (extractChannelResults && item.has("channelRenderer")) {
collector.commit(new YoutubeChannelInfoItemExtractor(
item.getObject("channelRenderer")));
} else if (extractPlaylistResults && item.has("playlistRenderer")) {
collector.commit(new YoutubePlaylistInfoItemExtractor(
item.getObject("playlistRenderer")));
} else if (extractPlaylistResults) {
if (item.has("playlistRenderer")) {
collector.commit(new YoutubePlaylistInfoItemExtractor(
item.getObject("playlistRenderer")));
} else if (item.has("showRenderer")) {
collector.commit(new YoutubeShowRendererInfoItemExtractor(
item.getObject("showRenderer")));
}
}
}
}

View File

@ -0,0 +1,57 @@
package org.schabi.newpipe.extractor.services.youtube.extractors;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import javax.annotation.Nonnull;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromObject;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/**
* A {@link YoutubeBaseShowInfoItemExtractor} implementation for {@code showRenderer}s.
*/
class YoutubeShowRendererInfoItemExtractor extends YoutubeBaseShowInfoItemExtractor {
@Nonnull
private final JsonObject shortBylineText;
@Nonnull
private final JsonObject longBylineText;
YoutubeShowRendererInfoItemExtractor(@Nonnull final JsonObject showRenderer) {
super(showRenderer);
this.shortBylineText = showRenderer.getObject("shortBylineText");
this.longBylineText = showRenderer.getObject("longBylineText");
}
@Override
public String getUploaderName() throws ParsingException {
String name = getTextFromObject(longBylineText);
if (isNullOrEmpty(name)) {
name = getTextFromObject(shortBylineText);
if (isNullOrEmpty(name)) {
throw new ParsingException("Could not get uploader name");
}
}
return name;
}
@Override
public String getUploaderUrl() throws ParsingException {
String uploaderUrl = getUrlFromObject(longBylineText);
if (uploaderUrl == null) {
uploaderUrl = getUrlFromObject(shortBylineText);
if (uploaderUrl == null) {
throw new ParsingException("Could not get uploader URL");
}
}
return uploaderUrl;
}
@Override
public boolean isUploaderVerified() throws ParsingException {
// We do not have this information in showRenderers
return false;
}
}

View File

@ -27,7 +27,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.CPN;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.RACY_CHECK_OK;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.VIDEO_ID;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.createDesktopPlayerBody;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.createTvHtml5EmbedPlayerBody;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateContentPlaybackNonce;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateTParameter;
@ -96,7 +96,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@ -104,22 +103,21 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class YoutubeStreamExtractor extends StreamExtractor {
private static boolean isAndroidClientFetchForced = false;
private static boolean isIosClientFetchForced = false;
private JsonObject playerResponse;
private JsonObject nextResponse;
@Nullable
private JsonObject html5StreamingData;
private JsonObject iosStreamingData;
@Nullable
private JsonObject androidStreamingData;
@Nullable
private JsonObject iosStreamingData;
private JsonObject tvHtml5SimplyEmbedStreamingData;
private JsonObject videoPrimaryInfoRenderer;
private JsonObject videoSecondaryInfoRenderer;
private JsonObject playerMicroFormatRenderer;
private JsonObject playerCaptionsTracklistRenderer;
private int ageLimit = -1;
private StreamType streamType;
@ -127,9 +125,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// URLs (with the cpn parameter).
// Also because a nonce should be unique, it should be different between clients used, so
// three different strings are used.
private String html5Cpn;
private String androidCpn;
private String iosCpn;
private String androidCpn;
private String tvHtml5SimplyEmbedCpn;
public YoutubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
super(service, linkHandler);
@ -323,7 +321,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return Long.parseLong(duration);
} catch (final Exception e) {
return getDurationFromFirstAdaptiveFormat(Arrays.asList(
html5StreamingData, androidStreamingData, iosStreamingData));
iosStreamingData, androidStreamingData, tvHtml5SimplyEmbedStreamingData));
}
}
@ -585,7 +583,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// Android client doesn't contain all available streams (mainly the WEBM ones)
return getManifestUrl(
"dash",
Arrays.asList(html5StreamingData, androidStreamingData));
Arrays.asList(androidStreamingData, tvHtml5SimplyEmbedStreamingData));
}
@Nonnull
@ -598,7 +596,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// Also, on videos, non-iOS clients don't have an HLS manifest URL in their player response
return getManifestUrl(
"hls",
Arrays.asList(iosStreamingData, html5StreamingData, androidStreamingData));
Arrays.asList(
iosStreamingData, androidStreamingData, tvHtml5SimplyEmbedStreamingData));
}
@Nonnull
@ -635,28 +634,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
getVideoStreamBuilderHelper(true), "video-only");
}
/**
* Try to deobfuscate a streaming URL and fall back to the given URL, because decryption may
* fail if YouTube changes break something.
*
* <p>
* This way a breaking change from YouTube does not result in a broken extractor.
* </p>
*
* @param streamingUrl the streaming URL to which deobfuscating its throttling parameter if
* there is one
* @param videoId the video ID to use when extracting JavaScript player code, if needed
*/
private String tryDeobfuscateThrottlingParameterOfUrl(@Nonnull final String streamingUrl,
@Nonnull final String videoId) {
try {
return YoutubeJavaScriptPlayerManager.getUrlWithThrottlingParameterDeobfuscated(
videoId, streamingUrl);
} catch (final ParsingException e) {
return streamingUrl;
}
}
@Override
@Nonnull
public List<SubtitlesStream> getSubtitlesDefault() throws ParsingException {
@ -670,9 +647,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// We cannot store the subtitles list because the media format may change
final List<SubtitlesStream> subtitlesToReturn = new ArrayList<>();
final JsonObject renderer = playerResponse.getObject("captions")
.getObject("playerCaptionsTracklistRenderer");
final JsonArray captionsArray = renderer.getArray("captionTracks");
final JsonArray captionsArray = playerCaptionsTracklistRenderer.getArray("captionTracks");
// TODO: use this to apply auto translation to different language from a source language
// final JsonArray autoCaptionsArray = renderer.getArray("translationLanguages");
@ -796,64 +771,70 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final Localization localization = getExtractorLocalization();
final ContentCountry contentCountry = getExtractorContentCountry();
html5Cpn = generateContentPlaybackNonce();
playerResponse = getJsonPostResponse(PLAYER,
createDesktopPlayerBody(
localization,
contentCountry,
videoId,
YoutubeJavaScriptPlayerManager.getSignatureTimestamp(videoId),
false,
html5Cpn),
localization);
final JsonObject webPlayerResponse = YoutubeParsingHelper.getWebPlayerResponse(
localization, contentCountry, videoId);
// Save the playerResponse from the player endpoint of the desktop internal API because
// there can be restrictions on the embedded player.
// E.g. if a video is age-restricted, the embedded player's playabilityStatus says that
// the video cannot be played outside of YouTube, but does not show the original message.
final JsonObject youtubePlayerResponse = playerResponse;
if (playerResponse == null) {
throw new ExtractionException("Could not get playerResponse");
if (isPlayerResponseNotValid(webPlayerResponse, videoId)) {
// Check the playability status, as private and deleted videos and invalid video IDs do
// not return the ID provided in the player response
// When the requested video is playable and a different video ID is returned, it has
// the OK playability status, meaning the ExtractionException after this check will be
// thrown
checkPlayabilityStatus(
webPlayerResponse, webPlayerResponse.getObject("playabilityStatus"));
throw new ExtractionException("Initial WEB player response is not valid");
}
final JsonObject playabilityStatus = playerResponse.getObject("playabilityStatus");
// Save the webPlayerResponse into playerResponse in the case the video cannot be played,
// so some metadata can be retrieved
playerResponse = webPlayerResponse;
final boolean isAgeRestricted = playabilityStatus.getString("reason", "")
// Use the player response from the player endpoint of the desktop internal API because
// there can be restrictions on videos in the embedded player.
// E.g. if a video is age-restricted, the embedded player's playabilityStatus says that
// the video cannot be played outside of YouTube, but does not show the original message.
final JsonObject playabilityStatus = webPlayerResponse.getObject("playabilityStatus");
final boolean isAgeRestricted = "login_required".equalsIgnoreCase(
playabilityStatus.getString("status"))
&& playabilityStatus.getString("reason", "")
.contains("age");
setStreamType();
if (!playerResponse.has(STREAMING_DATA)) {
if (isAgeRestricted) {
fetchTvHtml5EmbedJsonPlayer(contentCountry, localization, videoId);
// If no streams can be fetched in the TVHTML5 simply embed client, the video should be
// age-restricted, therefore throw an AgeRestrictedContentException explicitly.
if (tvHtml5SimplyEmbedStreamingData == null) {
throw new AgeRestrictedContentException(
"This age-restricted video cannot be watched.");
}
// Refresh the stream type because the stream type may be not properly known for
// age-restricted videos
setStreamType();
} else {
checkPlayabilityStatus(webPlayerResponse, playabilityStatus);
// Fetching successfully the iOS player is mandatory to get streams
fetchIosMobileJsonPlayer(contentCountry, localization, videoId);
try {
fetchTvHtml5EmbedJsonPlayer(contentCountry, localization, videoId);
fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId);
} catch (final Exception ignored) {
// Ignore exceptions related to ANDROID client fetch or parsing, as it is not
// compulsory to play contents
}
}
// Refresh the stream type because the stream type may be not properly known for
// age-restricted videos
setStreamType();
if (html5StreamingData == null && playerResponse.has(STREAMING_DATA)) {
html5StreamingData = playerResponse.getObject(STREAMING_DATA);
}
if (html5StreamingData == null) {
checkPlayabilityStatus(youtubePlayerResponse, playabilityStatus);
}
// The microformat JSON object of the content is not returned on the client we use to
// try to get streams of unavailable contents but is still returned on the WEB client,
// The microformat JSON object of the content is only returned on the WEB client,
// so we need to store it instead of getting it directly from the playerResponse
playerMicroFormatRenderer = youtubePlayerResponse.getObject("microformat")
playerMicroFormatRenderer = webPlayerResponse.getObject("microformat")
.getObject("playerMicroformatRenderer");
if (isPlayerResponseNotValid(playerResponse, videoId)) {
throw new ExtractionException("Initial player response is not valid");
}
final byte[] body = JsonWriter.string(
prepareDesktopJsonBuilder(localization, contentCountry)
.value(VIDEO_ID, videoId)
@ -862,29 +843,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
.done())
.getBytes(StandardCharsets.UTF_8);
nextResponse = getJsonPostResponse(NEXT, body, localization);
// streamType can only have LIVE_STREAM, POST_LIVE_STREAM and VIDEO_STREAM values (see
// setStreamType()), so this block will be run only for POST_LIVE_STREAM and VIDEO_STREAM
// values if fetching of the ANDROID client is not forced
if ((!isAgeRestricted && streamType != StreamType.LIVE_STREAM)
|| isAndroidClientFetchForced) {
try {
fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId);
} catch (final Exception ignored) {
// Ignore exceptions related to ANDROID client fetch or parsing, as it is not
// compulsory to play contents
}
}
if ((!isAgeRestricted && streamType == StreamType.LIVE_STREAM)
|| isIosClientFetchForced) {
try {
fetchIosMobileJsonPlayer(contentCountry, localization, videoId);
} catch (final Exception ignored) {
// Ignore exceptions related to IOS client fetch or parsing, as it is not
// compulsory to play contents
}
}
}
private void checkPlayabilityStatus(final JsonObject youtubePlayerResponse,
@ -902,17 +860,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
status = newPlayabilityStatus.getString("status");
final String reason = newPlayabilityStatus.getString("reason");
if (status.equalsIgnoreCase("login_required")) {
if (reason == null) {
final String message = newPlayabilityStatus.getArray("messages").getString(0);
if (message != null && message.contains("private")) {
throw new PrivateContentException("This video is private.");
}
} else if (reason.contains("age")) {
// No streams can be fetched, therefore throw an AgeRestrictedContentException
// explicitly.
throw new AgeRestrictedContentException(
"This age-restricted video cannot be watched.");
if (status.equalsIgnoreCase("login_required") && reason == null) {
final String message = newPlayabilityStatus.getArray("messages").getString(0);
if (message != null && message.contains("private")) {
throw new PrivateContentException("This video is private.");
}
}
@ -937,10 +888,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
if (detailedErrorMessage != null && detailedErrorMessage.contains("country")) {
throw new GeographicRestrictionException(
"This video is not available in client's country.");
} else if (detailedErrorMessage != null) {
throw new ContentNotAvailableException(detailedErrorMessage);
} else {
throw new ContentNotAvailableException(reason);
throw new ContentNotAvailableException(
Objects.requireNonNullElse(detailedErrorMessage, reason));
}
}
}
@ -959,29 +909,34 @@ public class YoutubeStreamExtractor extends StreamExtractor {
androidCpn = generateContentPlaybackNonce();
final byte[] mobileBody = JsonWriter.string(
prepareAndroidMobileJsonBuilder(localization, contentCountry)
.object("playerRequest")
.value(VIDEO_ID, videoId)
.end()
.value("disablePlayerResponse", false)
.value(VIDEO_ID, videoId)
.value(CPN, androidCpn)
.value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true)
// Workaround getting streaming URLs which return 403 HTTP response code by
// using some parameters for Android client requests
.value("params", "CgIIAQ%3D%3D")
.done())
.getBytes(StandardCharsets.UTF_8);
final JsonObject androidPlayerResponse = getJsonAndroidPostResponse(PLAYER,
mobileBody, localization, "&t=" + generateTParameter()
+ "&id=" + videoId);
final JsonObject androidPlayerResponse = getJsonAndroidPostResponse(
"reel/reel_item_watch",
mobileBody,
localization,
"&t=" + generateTParameter() + "&id=" + videoId + "&$fields=playerResponse");
if (isPlayerResponseNotValid(androidPlayerResponse, videoId)) {
final JsonObject playerResponseObject = androidPlayerResponse.getObject("playerResponse");
if (isPlayerResponseNotValid(playerResponseObject, videoId)) {
return;
}
final JsonObject streamingData = androidPlayerResponse.getObject(STREAMING_DATA);
final JsonObject streamingData = playerResponseObject.getObject(STREAMING_DATA);
if (!isNullOrEmpty(streamingData)) {
androidStreamingData = streamingData;
if (html5StreamingData == null) {
playerResponse = androidPlayerResponse;
if (isNullOrEmpty(playerCaptionsTracklistRenderer)) {
playerCaptionsTracklistRenderer = playerResponseObject.getObject("captions")
.getObject("playerCaptionsTracklistRenderer");
}
}
}
@ -1009,15 +964,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
+ "&id=" + videoId);
if (isPlayerResponseNotValid(iosPlayerResponse, videoId)) {
return;
throw new ExtractionException("IOS player response is not valid");
}
final JsonObject streamingData = iosPlayerResponse.getObject(STREAMING_DATA);
if (!isNullOrEmpty(streamingData)) {
iosStreamingData = streamingData;
if (html5StreamingData == null) {
playerResponse = iosPlayerResponse;
}
playerCaptionsTracklistRenderer = iosPlayerResponse.getObject("captions")
.getObject("playerCaptionsTracklistRenderer");
}
}
@ -1034,26 +988,25 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final Localization localization,
@Nonnull final String videoId)
throws IOException, ExtractionException {
// Because a cpn is unique to each request, we need to generate it again
html5Cpn = generateContentPlaybackNonce();
tvHtml5SimplyEmbedCpn = generateContentPlaybackNonce();
final JsonObject tvHtml5EmbedPlayerResponse = getJsonPostResponse(PLAYER,
createDesktopPlayerBody(localization,
createTvHtml5EmbedPlayerBody(localization,
contentCountry,
videoId,
YoutubeJavaScriptPlayerManager.getSignatureTimestamp(videoId),
true,
html5Cpn), localization);
tvHtml5SimplyEmbedCpn), localization);
if (isPlayerResponseNotValid(tvHtml5EmbedPlayerResponse, videoId)) {
return;
throw new ExtractionException("TVHTML5 embed player response is not valid");
}
final JsonObject streamingData = tvHtml5EmbedPlayerResponse.getObject(
STREAMING_DATA);
final JsonObject streamingData = tvHtml5EmbedPlayerResponse.getObject(STREAMING_DATA);
if (!isNullOrEmpty(streamingData)) {
playerResponse = tvHtml5EmbedPlayerResponse;
html5StreamingData = streamingData;
tvHtml5SimplyEmbedStreamingData = streamingData;
playerCaptionsTracklistRenderer = playerResponse.getObject("captions")
.getObject("playerCaptionsTracklistRenderer");
}
}
@ -1145,14 +1098,20 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final List<T> streamList = new ArrayList<>();
java.util.stream.Stream.of(
// Use the androidStreamingData object first because there is no n param and no
// signatureCiphers in streaming URLs of the Android client
/*
Use the iosStreamingData object first because there is no n param and no
signatureCiphers in streaming URLs of the iOS client
The androidStreamingData is used as second way as it isn't used on livestreams,
it doesn't return all available streams, and the Android client extraction is
more likely to break
As age-restricted videos are not common, use tvHtml5SimplyEmbedStreamingData
last, which will be the only one not empty for age-restricted content
*/
new Pair<>(iosStreamingData, iosCpn),
new Pair<>(androidStreamingData, androidCpn),
new Pair<>(html5StreamingData, html5Cpn),
// Use the iosStreamingData object in the last position because most of the
// available streams can be extracted with the Android and web clients and also
// because the iOS client is only enabled by default on livestreams
new Pair<>(iosStreamingData, iosCpn)
new Pair<>(tvHtml5SimplyEmbedStreamingData, tvHtml5SimplyEmbedCpn)
)
.flatMap(pair -> getStreamsFromStreamingDataKey(videoId, pair.getFirst(),
streamingDataKey, itagTypeWanted, pair.getSecond()))
@ -1304,8 +1263,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return buildAndAddItagInfoToList(videoId, formatData, itagItem,
itagItem.itagType, contentPlaybackNonce);
}
} catch (final IOException | ExtractionException ignored) {
// if the itag is not supported and getItag fails, we end up here
} catch (final ExtractionException ignored) {
// If the itag is not supported, the n parameter of HTML5 clients cannot be
// decoded or buildAndAddItagInfoToList fails, we end up here
}
return null;
})
@ -1317,26 +1277,31 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final JsonObject formatData,
@Nonnull final ItagItem itagItem,
@Nonnull final ItagItem.ItagType itagType,
@Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException {
@Nonnull final String contentPlaybackNonce) throws ExtractionException {
String streamUrl;
if (formatData.has("url")) {
streamUrl = formatData.getString("url");
} else {
// This url has an obfuscated signature
final String cipherString = formatData.has(CIPHER)
? formatData.getString(CIPHER)
: formatData.getString(SIGNATURE_CIPHER);
final Map<String, String> cipher = Parser.compatParseMap(
cipherString);
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
+ YoutubeJavaScriptPlayerManager.deobfuscateSignature(videoId, cipher.get("s"));
final String cipherString = formatData.getString(CIPHER,
formatData.getString(SIGNATURE_CIPHER));
final var cipher = Parser.compatParseMap(cipherString);
final String signature = YoutubeJavaScriptPlayerManager.deobfuscateSignature(videoId,
cipher.getOrDefault("s", ""));
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" + signature;
}
// Add the content playback nonce to the stream URL
streamUrl += "&" + CPN + "=" + contentPlaybackNonce;
// Decrypt the n parameter if it is present
streamUrl = tryDeobfuscateThrottlingParameterOfUrl(streamUrl, videoId);
// Decode the n parameter if it is present
// If it cannot be decoded, the stream cannot be used as streaming URLs return HTTP 403
// responses if it has not the right value
// Exceptions thrown by
// YoutubeJavaScriptPlayerManager.getUrlWithThrottlingParameterDeobfuscated are so
// propagated to the parent which ignores streams in this case
streamUrl = YoutubeJavaScriptPlayerManager.getUrlWithThrottlingParameterDeobfuscated(
videoId, streamUrl);
final JsonObject initRange = formatData.getObject("initRange");
final JsonObject indexRange = formatData.getObject("indexRange");
@ -1601,42 +1566,4 @@ public class YoutubeStreamExtractor extends StreamExtractor {
.getObject("results")
.getArray("contents"));
}
/**
* Enable or disable the fetch of the Android client for all stream types.
*
* <p>
* By default, the fetch of the Android client will be made only on videos, in order to reduce
* data usage, because available streams of the Android client will be almost equal to the ones
* available on the {@code WEB} client: you can get exclusively a 48kbps audio stream and a
* 3GPP very low stream (which is, most of times, a 144p8 stream).
* </p>
*
* @param forceFetchAndroidClientValue whether to always fetch the Android client and not only
* for videos
*/
public static void forceFetchAndroidClient(final boolean forceFetchAndroidClientValue) {
isAndroidClientFetchForced = forceFetchAndroidClientValue;
}
/**
* Enable or disable the fetch of the iOS client for all stream types.
*
* <p>
* By default, the fetch of the iOS client will be made only on livestreams, in order to get an
* HLS manifest with separated audio and video which has also an higher replay time (up to one
* hour, depending of the content instead of 30 seconds with non-iOS clients).
* </p>
*
* <p>
* Enabling this option will allow you to get an HLS manifest also for regular videos, which
* contains resolutions up to 1080p60.
* </p>
*
* @param forceFetchIosClientValue whether to always fetch the iOS client and not only for
* livestreams
*/
public static void forceFetchIosClient(final boolean forceFetchIosClientValue) {
isIosClientFetchForced = forceFetchIosClientValue;
}
}

View File

@ -6,7 +6,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import java.io.UnsupportedEncodingException;
import java.util.List;
import javax.annotation.Nonnull;
@ -40,31 +39,22 @@ public final class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFa
@Nonnull final List<String> contentFilters,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
try {
if (!contentFilters.isEmpty()) {
final String contentFilter = contentFilters.get(0);
switch (contentFilter) {
case VIDEOS:
return SEARCH_URL + encodeUrlUtf8(searchString)
+ "&sp=EgIQAfABAQ%253D%253D";
case CHANNELS:
return SEARCH_URL + encodeUrlUtf8(searchString)
+ "&sp=EgIQAvABAQ%253D%253D";
case PLAYLISTS:
return SEARCH_URL + encodeUrlUtf8(searchString)
+ "&sp=EgIQA_ABAQ%253D%253D";
case MUSIC_SONGS:
case MUSIC_VIDEOS:
case MUSIC_ALBUMS:
case MUSIC_PLAYLISTS:
case MUSIC_ARTISTS:
return MUSIC_SEARCH_URL + encodeUrlUtf8(searchString);
}
}
return SEARCH_URL + encodeUrlUtf8(searchString) + "&sp=8AEB";
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("Could not encode query", e);
final String contentFilter = !contentFilters.isEmpty() ? contentFilters.get(0) : "";
switch (contentFilter) {
case VIDEOS:
return SEARCH_URL + encodeUrlUtf8(searchString) + "&sp=EgIQAfABAQ%253D%253D";
case CHANNELS:
return SEARCH_URL + encodeUrlUtf8(searchString) + "&sp=EgIQAvABAQ%253D%253D";
case PLAYLISTS:
return SEARCH_URL + encodeUrlUtf8(searchString) + "&sp=EgIQA_ABAQ%253D%253D";
case MUSIC_SONGS:
case MUSIC_VIDEOS:
case MUSIC_ALBUMS:
case MUSIC_PLAYLISTS:
case MUSIC_ARTISTS:
return MUSIC_SEARCH_URL + encodeUrlUtf8(searchString);
default:
return SEARCH_URL + encodeUrlUtf8(searchString) + "&sp=8AEB";
}
}

View File

@ -22,11 +22,11 @@ package org.schabi.newpipe.extractor.utils;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Arrays;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
@ -121,17 +121,12 @@ public final class Parser {
}
@Nonnull
public static Map<String, String> compatParseMap(@Nonnull final String input)
throws UnsupportedEncodingException {
final Map<String, String> map = new HashMap<>();
for (final String arg : input.split("&")) {
final String[] splitArg = arg.split("=");
if (splitArg.length > 1) {
map.put(splitArg[0], Utils.decodeUrlUtf8(splitArg[1]));
} else {
map.put(splitArg[0], "");
}
}
return map;
public static Map<String, String> compatParseMap(@Nonnull final String input) {
return Arrays.stream(input.split("&"))
.map(arg -> arg.split("="))
.filter(splitArg -> splitArg.length > 1)
.collect(Collectors.toMap(splitArg -> splitArg[0],
splitArg -> Utils.decodeUrlUtf8(splitArg[1]),
(existing, replacement) -> replacement));
}
}

View File

@ -2,7 +2,6 @@ package org.schabi.newpipe.extractor.utils;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
@ -33,22 +32,18 @@ public final class Utils {
*
* @param string The string to be encoded.
* @return The encoded URL.
* @throws UnsupportedEncodingException This shouldn't be thrown, as UTF-8 should be supported.
*/
public static String encodeUrlUtf8(final String string) throws UnsupportedEncodingException {
// TODO: Switch to URLEncoder.encode(String, Charset) in Java 10.
return URLEncoder.encode(string, StandardCharsets.UTF_8.name());
public static String encodeUrlUtf8(final String string) {
return URLEncoder.encode(string, StandardCharsets.UTF_8);
}
/**
* Decodes a URL using the UTF-8 character set.
* @param url The URL to be decoded.
* @return The decoded URL.
* @throws UnsupportedEncodingException This shouldn't be thrown, as UTF-8 should be supported.
*/
public static String decodeUrlUtf8(final String url) throws UnsupportedEncodingException {
// TODO: Switch to URLDecoder.decode(String, Charset) in Java 10.
return URLDecoder.decode(url, StandardCharsets.UTF_8.name());
public static String decodeUrlUtf8(final String url) {
return URLDecoder.decode(url, StandardCharsets.UTF_8);
}
/**
@ -155,22 +150,10 @@ public final class Utils {
if (urlQuery != null) {
for (final String param : urlQuery.split("&")) {
final String[] params = param.split("=", 2);
String query;
try {
query = decodeUrlUtf8(params[0]);
} catch (final UnsupportedEncodingException e) {
// Cannot decode string with UTF-8, using the string without decoding
query = params[0];
}
final String query = decodeUrlUtf8(params[0]);
if (query.equals(parameterName)) {
try {
return decodeUrlUtf8(params[1]);
} catch (final UnsupportedEncodingException e) {
// Cannot decode string with UTF-8, using the string without decoding
return params[1];
}
return decodeUrlUtf8(params[1]);
}
}
}

View File

@ -39,6 +39,5 @@ public class BandcampCommentsLinkHandlerFactoryTest {
assertTrue(linkHandler.acceptUrl("http://ZachBenson.Bandcamp.COM/Track/U-I-Tonite/"));
assertTrue(linkHandler.acceptUrl("https://interovgm.bandcamp.com/track/title"));
assertTrue(linkHandler.acceptUrl("https://goodgoodblood-tl.bandcamp.com/track/when-it-all-wakes-up"));
assertTrue(linkHandler.acceptUrl("https://lobstertheremin.com/track/unfinished"));
}
}

View File

@ -28,7 +28,7 @@ public class BandcampFeaturedLinkHandlerFactoryTest {
assertTrue(linkHandler.acceptUrl("https://bandcamp.com/?show=1"));
assertTrue(linkHandler.acceptUrl("http://bandcamp.com/?show=2"));
assertTrue(linkHandler.acceptUrl("https://bandcamp.com/api/mobile/24/bootstrap_data"));
assertTrue(linkHandler.acceptUrl("https://bandcamp.com/api/bcweekly/1/list"));
assertTrue(linkHandler.acceptUrl("https://bandcamp.com/api/bcweekly/3/list"));
assertFalse(linkHandler.acceptUrl("https://bandcamp.com/?show="));
assertFalse(linkHandler.acceptUrl("https://bandcamp.com/?show=a"));
@ -38,7 +38,7 @@ public class BandcampFeaturedLinkHandlerFactoryTest {
@Test
public void testGetUrl() throws ParsingException {
assertEquals("https://bandcamp.com/api/mobile/24/bootstrap_data", linkHandler.getUrl("Featured"));
assertEquals("https://bandcamp.com/api/bcweekly/1/list", linkHandler.getUrl("Radio"));
assertEquals("https://bandcamp.com/api/bcweekly/3/list", linkHandler.getUrl("Radio"));
}
@Test
@ -46,7 +46,7 @@ public class BandcampFeaturedLinkHandlerFactoryTest {
assertEquals("Featured", linkHandler.getId("http://bandcamp.com/api/mobile/24/bootstrap_data"));
assertEquals("Featured", linkHandler.getId("https://bandcamp.com/api/mobile/24/bootstrap_data"));
assertEquals("Radio", linkHandler.getId("http://bandcamp.com/?show=1"));
assertEquals("Radio", linkHandler.getId("https://bandcamp.com/api/bcweekly/1/list"));
assertEquals("Radio", linkHandler.getId("https://bandcamp.com/api/bcweekly/3/list"));
}
}

View File

@ -8,6 +8,7 @@ import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.BaseListExtractorTest;
import org.schabi.newpipe.extractor.services.DefaultTests;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -35,15 +36,14 @@ public class BandcampRadioExtractorTest implements BaseListExtractorTest {
}
@Test
public void testRadioCount() throws ExtractionException, IOException {
public void testRadioCount() {
final List<StreamInfoItem> list = extractor.getInitialPage().getItems();
assertTrue(list.size() > 300);
}
@Test
public void testRelatedItems() throws Exception {
// DefaultTests.defaultTestRelatedItems(extractor);
// Would fail because BandcampRadioInfoItemExtractor.getUploaderName() returns an empty String
DefaultTests.defaultTestRelatedItems(extractor);
}
@Test
@ -68,11 +68,11 @@ public class BandcampRadioExtractorTest implements BaseListExtractorTest {
@Test
public void testUrl() throws Exception {
assertEquals("https://bandcamp.com/api/bcweekly/1/list", extractor.getUrl());
assertEquals("https://bandcamp.com/api/bcweekly/3/list", extractor.getUrl());
}
@Test
public void testOriginalUrl() throws Exception {
assertEquals("https://bandcamp.com/api/bcweekly/1/list", extractor.getOriginalUrl());
assertEquals("https://bandcamp.com/api/bcweekly/3/list", extractor.getOriginalUrl());
}
}

View File

@ -49,6 +49,5 @@ public class BandcampStreamLinkHandlerFactoryTest {
assertTrue(linkHandler.acceptUrl("https://interovgm.bandcamp.com/track/title"));
assertTrue(linkHandler.acceptUrl("http://bandcamP.com/?show=38"));
assertTrue(linkHandler.acceptUrl("https://goodgoodblood-tl.bandcamp.com/track/when-it-all-wakes-up"));
assertTrue(linkHandler.acceptUrl("https://lobstertheremin.com/track/unfinished"));
}
}

View File

@ -48,7 +48,7 @@ public class PeertubeCommentsExtractorTest {
@Test
void testGetCommentsFromCommentsInfo() throws IOException, ExtractionException {
final String comment = "Thanks for this nice video explanation of Peertube!";
final String comment = "I love this. ❤";
final CommentsInfo commentsInfo =
CommentsInfo.getInfo("https://framatube.org/w/kkGMgK9ZtnKfYAgnEtQxbv");

View File

@ -85,7 +85,7 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
@Override public long expectedViewCountAtLeast() { return 38600; }
@Nullable @Override public String expectedUploadDate() { return "2018-10-01 10:52:46.396"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2018-10-01T10:52:46.396Z"; }
@Override public long expectedLikeCountAtLeast() { return 19; }
@Override public long expectedLikeCountAtLeast() { return 18; }
@Override public long expectedDislikeCountAtLeast() { return 0; }
@Override public String expectedHost() { return "framatube.org"; }
@Override public String expectedCategory() { return "Science & Technology"; }

View File

@ -25,6 +25,10 @@ class SoundcloudParsingHelperTest {
void resolveIdWithWidgetApiTest() throws Exception {
assertEquals("26057743", SoundcloudParsingHelper.resolveIdWithWidgetApi("https://soundcloud.com/trapcity"));
assertEquals("16069159", SoundcloudParsingHelper.resolveIdWithWidgetApi("https://soundcloud.com/nocopyrightsounds"));
assertEquals("26057743", SoundcloudParsingHelper.resolveIdWithWidgetApi("https://on.soundcloud.com/Rr2JyfFcYwbawpw49"));
assertEquals("1818813498", SoundcloudParsingHelper.resolveIdWithWidgetApi("https://on.soundcloud.com/a8QmYdMnmxnsSTEp9"));
assertEquals("1468401502", SoundcloudParsingHelper.resolveIdWithWidgetApi("https://on.soundcloud.com/rdt7e"));
}
}

View File

@ -317,7 +317,7 @@ public class SoundcloudPlaylistExtractorTest {
@Test
public void testUploaderName() {
assertEquals("user350509423", extractor.getUploaderName());
assertEquals("Chaazyy", extractor.getUploaderName());
}
@Test

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.extractor.services.soundcloud.search;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.services.DefaultTests.assertNoDuplicatedItems;
@ -23,7 +22,6 @@ import org.schabi.newpipe.extractor.services.DefaultSearchExtractorTest;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import javax.annotation.Nullable;
@ -46,8 +44,8 @@ public class SoundcloudSearchExtractorTest {
@Override public StreamingService expectedService() { return SoundCloud; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "soundcloud.com/search?q=" + urlEncode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search?q=" + urlEncode(QUERY); }
@Override public String expectedUrlContains() { return "soundcloud.com/search?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
// @formatter:on
@ -69,8 +67,8 @@ public class SoundcloudSearchExtractorTest {
@Override public StreamingService expectedService() { return SoundCloud; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "soundcloud.com/search/tracks?q=" + urlEncode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/tracks?q=" + urlEncode(QUERY); }
@Override public String expectedUrlContains() { return "soundcloud.com/search/tracks?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/tracks?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@ -93,8 +91,8 @@ public class SoundcloudSearchExtractorTest {
@Override public StreamingService expectedService() { return SoundCloud; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "soundcloud.com/search/users?q=" + urlEncode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/users?q=" + urlEncode(QUERY); }
@Override public String expectedUrlContains() { return "soundcloud.com/search/users?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/users?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.CHANNEL; }
@ -117,8 +115,8 @@ public class SoundcloudSearchExtractorTest {
@Override public StreamingService expectedService() { return SoundCloud; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "soundcloud.com/search/playlists?q=" + urlEncode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/playlists?q=" + urlEncode(QUERY); }
@Override public String expectedUrlContains() { return "soundcloud.com/search/playlists?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/playlists?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.PLAYLIST; }
@ -139,14 +137,6 @@ public class SoundcloudSearchExtractorTest {
}
}
private static String urlEncode(String value) {
try {
return Utils.encodeUrlUtf8(value);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static class UserVerified extends DefaultSearchExtractorTest {
private static SearchExtractor extractor;
private static final String QUERY = "David Guetta";
@ -162,8 +152,8 @@ public class SoundcloudSearchExtractorTest {
@Override public StreamingService expectedService() { return SoundCloud; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "soundcloud.com/search/users?q=" + urlEncode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/users?q=" + urlEncode(QUERY); }
@Override public String expectedUrlContains() { return "soundcloud.com/search/users?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search/users?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@ -200,8 +190,8 @@ public class SoundcloudSearchExtractorTest {
@Override public StreamingService expectedService() throws Exception { return SoundCloud; }
@Override public String expectedName() throws Exception { return QUERY; }
@Override public String expectedId() throws Exception { return QUERY; }
@Override public String expectedUrlContains() { return "soundcloud.com/search?q=" + urlEncode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search?q=" + urlEncode(QUERY); }
@Override public String expectedUrlContains() { return "soundcloud.com/search?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedOriginalUrlContains() { return "soundcloud.com/search?q=" + Utils.encodeUrlUtf8(QUERY); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
}

View File

@ -294,7 +294,7 @@ public class YoutubeChannelExtractorTest {
@Test
public void testDescription() throws Exception {
assertContains("Our World is Amazing. \n\nQuestions? Ideas? Tweet me:", extractor.getDescription());
assertContains("Our World is Amazing", extractor.getDescription());
}
@Test

View File

@ -428,7 +428,7 @@ public class YoutubeSearchExtractorTest {
*/
public static class CrisisResources extends DefaultSearchExtractorTest {
private static SearchExtractor extractor;
private static final String QUERY = "blue whale";
private static final String QUERY = "suicide";
@BeforeAll
public static void setUp() throws Exception {

View File

@ -126,7 +126,7 @@ public class YoutubeStreamExtractorDefaultTest {
@BeforeAll
public static void setUp() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "pewdiwpie"));
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "pewdiepie"));
extractor = YouTube.getStreamExtractor(URL);
extractor.fetchPage();
}
@ -502,7 +502,7 @@ public class YoutubeStreamExtractorDefaultTest {
@BeforeAll
public static void setUp() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderTestImpl.getInstance());
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "unlistedDefaultTest"));
extractor = (YoutubeStreamExtractor) YouTube
.getStreamExtractor("https://www.youtube.com/watch?v=tjz2u2DiveM");
extractor.fetchPage();
@ -522,7 +522,7 @@ public class YoutubeStreamExtractorDefaultTest {
@BeforeAll
public static void setUp() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderTestImpl.getInstance());
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "ccLicensed"));
extractor = YouTube.getStreamExtractor(URL);
extractor.fetchPage();
}

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:32:05 GMT"
"Thu, 18 Jul 2024 18:09:57 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:32:05 GMT"
"Thu, 18 Jul 2024 18:09:57 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003drP1g96D6iSY; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:32:05 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dAc2NF4wLV18; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 18:09:57 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,10 +3,10 @@
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Accept-Language": [
@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:16 GMT"
"Thu, 18 Jul 2024 17:47:54 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:16 GMT"
"Thu, 18 Jul 2024 17:47:54 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dR5CBjvYjQSM; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dQ-hpT9jfKtU; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:47:54 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -3,17 +3,17 @@
"httpMethod": "POST",
"url": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Cookie": [
"SOCS\u003dCAE\u003d"
],
"X-YouTube-Client-Version": [
"2.20240410.01.00"
"2.20240718.01.00"
],
"X-YouTube-Client-Name": [
"1"
@ -230,9 +230,9 @@
50,
52,
48,
52,
55,
49,
48,
56,
46,
48,
49,
@ -362,7 +362,7 @@
"application/json; charset\u003dUTF-8"
],
"date": [
"Wed, 10 Apr 2024 17:31:16 GMT"
"Thu, 18 Jul 2024 17:47:56 GMT"
],
"server": [
"scaffolding on HTTPServer2"
@ -382,7 +382,7 @@
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtnNEJUcEpIazMtbyjkmduwBjIOCgJGUhIIEgQSAgsMIGc%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240410.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xca7be53690c60dbf\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23776346,23804281,23946420,23966208,23983296,23986016,23998056,24004644,24036948,24077241,24080738,24120819,24135310,24166867,24181174,24187377,24241378,24290971,24377598,24439361,24451319,24453989,24468724,24506784,24515388,24515423,24524098,24524562,24525811,24542367,24548627,24548629,24550458,24560416,24566687,24697069,24699899,39325854,39325978,51003636,51006181,51009781,51010235,51012659,51016856,51017346,51019626,51020570,51025415,51026715,51027870,51030101,51033399,51033765,51037344,51037353,51038805,51039200,51043774,51048489,51050361,51053689,51056364,51057501,51057846,51057853,51059573,51060353,51063643,51064835,51069269,51072748,51074183,51079239,51080343,51089175,51089441,51089956,51091331,51091810,51091897,51092661,51094173,51094195,51094202,51094207,51095478,51096577,51096646,51098297,51098299,51100401,51101454,51103044,51103858,51105630,51105690,51105868,51106995,51107336,51108006,51109078,51109704,51111738,51113658,51113663,51115184,51116067,51117319,51118932,51119507,51119509,51119511,51120529,51120721,51123073,51124104,51124304,51124405,51124478,51127560,51128585,51129210,51129216,51129218,51129220,51129222,51129224,51129395,51131429,51132393,51133103,51135346,51135656,51136218,51136785,51136924,51139379,51140749,51140806,51141539,51141798,51142840,51142842,51142886,51142889,51144925,51145052,51145216,51145258,51145749,51146567,51146962,51147554,51147789,51147897,51148750,51148974,51148983,51149504,51151572,51152043,51152050,51152207,51152530,51153775,51154012,51155674,51156055,51156581,51157410,51157729,51157841,51158470,51158513,51159916,51160594\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240410\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIivSxv5a4hQMVaivxBR1E3QYaMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UC6nSFpj9HTCZ5t-N3Rm3-HA\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtzVEVQR1dURHphWSjMquW0BjIiCgJGUhIcEhgSFgsMDg8QERITFBUWFxgZGhscHR4fICEgGA%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240718.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xd625b21ac0ea5a53\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23703445,23804281,23946420,23966208,23986028,23998056,24004644,24077241,24166867,24181174,24241378,24290971,24439361,24453989,24456089,24468724,24499533,24542367,24548627,24548629,24550458,24566687,24690004,24699899,39325854,39326848,39326916,51009781,51010235,51016856,51017346,51020570,51025415,51030101,51037346,51037353,51041512,51050361,51053689,51057848,51057851,51060353,51063643,51064835,51072748,51091058,51091331,51095478,51098297,51098299,51102410,51105630,51111738,51113658,51113661,51115184,51116067,51117319,51118932,51121939,51124104,51133103,51139379,51141472,51148688,51148974,51148983,51149607,51150450,51152050,51157411,51157430,51157432,51157838,51158470,51158514,51160545,51162170,51163635,51165467,51165568,51169117,51170249,51172670,51172684,51172691,51172700,51172707,51172716,51172723,51172728,51173021,51173508,51175606,51176511,51178310,51178333,51178340,51178357,51178706,51178982,51182275,51183508,51183909,51184022,51184990,51186528,51186670,51189826,51190059,51190075,51190078,51190085,51190198,51190213,51190220,51190229,51190652,51193591,51194137,51195231,51196476,51196769,51197569,51197687,51197694,51197697,51197708,51199193,51200251,51200256,51200295,51200298,51200568,51201350,51201363,51201372,51201381,51201428,51201435,51201444,51201451,51201814,51203141,51203200,51204329,51204587,51204938,51207174,51207191,51207196,51207209,51209172,51210770,51211461,51212464,51212553,51212567,51213807,51217504,51219800,51221011,51221152,51222152,51222695,51223962,51224134,51224747,51224922,51225437,51226344,51227408,51227772,51227881,51227902,51228202,51228349,51228351,51228695,51228771,51228776,51228787,51228796,51228805,51228814,51229628,51230124,51230423,51230478,51230492,51232125,51232143,51232230,51233332,51235147,51237540,51238400,51238514,51238569,51238736,51240880,51240888,51241028,51241600\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240718\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIusK-7pKxhwMVBDPxBR08yQnhMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UC6nSFpj9HTCZ5t-N3Rm3-HA\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse"
}
}

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:11 GMT"
"Thu, 18 Jul 2024 17:49:35 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:11 GMT"
"Thu, 18 Jul 2024 17:49:35 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dFBJkWNhWCv0; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:11 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dZHHPT-DrXJQ; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:49:35 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:11 GMT"
"Thu, 18 Jul 2024 17:48:47 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:11 GMT"
"Thu, 18 Jul 2024 17:48:47 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dVQZ7dBDq_80; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:11 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003d9_8OVS-ITHc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:48:47 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -13,7 +13,7 @@
"SOCS\u003dCAE\u003d"
],
"X-YouTube-Client-Version": [
"2.20240410.01.00"
"2.20240718.01.00"
],
"X-YouTube-Client-Name": [
"1"
@ -230,9 +230,9 @@
50,
52,
48,
52,
55,
49,
48,
56,
46,
48,
49,
@ -374,7 +374,7 @@
"application/json; charset\u003dUTF-8"
],
"date": [
"Wed, 10 Apr 2024 17:31:11 GMT"
"Thu, 18 Jul 2024 17:48:48 GMT"
],
"server": [
"scaffolding on HTTPServer2"
@ -394,7 +394,7 @@
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgsweWlSMmxESC15SSjfmduwBjIOCgJGUhIIEgQSAgsMIFU%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240410.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x322622f38822bd3d\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"9407156,23804281,23946420,23966208,23983296,23986016,23998056,24004644,24036948,24077241,24080738,24120819,24135310,24166867,24181174,24187377,24241378,24290971,24377598,24439361,24451319,24453989,24457856,24459435,24468724,24506784,24515389,24515423,24524098,24524562,24525812,24542367,24543668,24548627,24548629,24550458,24560416,24566687,24690004,24697013,24697067,24699899,39325854,39325972,39325978,51003636,51006181,51009781,51010235,51012659,51016856,51017346,51019626,51020570,51025415,51026715,51027870,51030101,51033399,51033765,51037346,51037351,51038805,51039200,51048489,51050361,51053689,51057501,51057842,51057855,51057865,51059571,51060353,51063643,51064835,51069269,51072748,51074183,51079239,51080342,51089175,51089441,51089956,51091331,51091897,51092660,51094173,51094200,51094205,51095478,51096577,51096646,51098297,51098299,51099083,51100401,51101454,51103858,51105630,51105868,51106995,51107340,51107658,51108006,51109078,51111738,51113656,51113661,51113750,51115184,51116067,51118932,51119507,51119509,51119511,51120895,51122807,51123077,51124104,51127342,51128585,51129028,51129216,51129218,51129220,51129222,51129224,51129395,51132393,51133103,51134014,51135027,51135346,51135654,51136218,51136785,51137135,51138687,51139379,51140749,51140806,51141798,51142840,51142842,51142882,51142889,51144926,51145052,51145190,51145216,51145749,51145849,51146568,51146960,51146962,51147101,51147558,51147789,51147896,51148749,51148976,51148981,51152043,51152050,51152207,51152530,51152833,51153775,51154012,51156215,51156581,51157411,51157838,51158470,51158514,51159069,51159917,51161868\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240410\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIgZiPvZa4hQMVsUFPBB2kBAyQMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCEOXxzW2vU0P-0THehuIIeg\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtiVTZPVEtWdkUtayiAq-W0BjIiCgJGUhIcEhgSFgsMDg8QERITFBUWFxgZGhscHR4fICEgMg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240718.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xe140936d506561d9\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,23946420,23966208,23986015,23998056,24004644,24077241,24108448,24166867,24181174,24241378,24290971,24439361,24453989,24456089,24468724,24542367,24543669,24548627,24548629,24550458,24566687,24699899,39325854,39326848,39326916,51009781,51010235,51016856,51017346,51020570,51025415,51030101,51037342,51037349,51041512,51050361,51053689,51057846,51057851,51060353,51063643,51064835,51072748,51087998,51091058,51091331,51095478,51098297,51098299,51102409,51111738,51113656,51113663,51115184,51116067,51118932,51124104,51126280,51131738,51133103,51139379,51141472,51144926,51148688,51148978,51148985,51149422,51149607,51150448,51152050,51153490,51157411,51157430,51157432,51157838,51157895,51158470,51158514,51160545,51160817,51162170,51163641,51165467,51165568,51170247,51172670,51172686,51172691,51172702,51172705,51172716,51172721,51172730,51173021,51175606,51176511,51177817,51178310,51178331,51178348,51178355,51178706,51178982,51182275,51183910,51184022,51184990,51186528,51186669,51186752,51189826,51190059,51190073,51190082,51190085,51190200,51190209,51190216,51190231,51190652,51195231,51196476,51196769,51197685,51197694,51197697,51197704,51199193,51199719,51200184,51200249,51200260,51200295,51200300,51200568,51201352,51201367,51201372,51201383,51201426,51201437,51201442,51201447,51201814,51203200,51204329,51204585,51207178,51207187,51207200,51207215,51211461,51212466,51212545,51212551,51212567,51213714,51213807,51213887,51217504,51219800,51221011,51221152,51222759,51222972,51223961,51224134,51224655,51224747,51224921,51225437,51226344,51226709,51227410,51227776,51227880,51227902,51228202,51228349,51228352,51228695,51228769,51228774,51228785,51228800,51228809,51228818,51229233,51230123,51230477,51230492,51231813,51232109,51232143,51236268,51237540,51237884,51238568,51238737,51240880,51240890,51241028,51241658,51241862\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240718\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMImfCdh5OxhwMVgEtPBB1-bw1fMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCEOXxzW2vU0P-0THehuIIeg\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse"
}
}

View File

@ -3,10 +3,10 @@
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Accept-Language": [
@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:12 GMT"
"Thu, 18 Jul 2024 17:49:19 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:12 GMT"
"Thu, 18 Jul 2024 17:49:19 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dRUU709V770o; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:12 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dauHnMsX2TkQ; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:49:19 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -3,10 +3,10 @@
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Accept-Language": [
@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:12 GMT"
"Thu, 18 Jul 2024 17:47:35 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:12 GMT"
"Thu, 18 Jul 2024 17:47:35 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dVFl0_DSlGaw; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:12 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003doTOG10oOeIY; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:47:35 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -3,17 +3,17 @@
"httpMethod": "POST",
"url": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Cookie": [
"SOCS\u003dCAE\u003d"
],
"X-YouTube-Client-Version": [
"2.20240410.01.00"
"2.20240718.01.00"
],
"X-YouTube-Client-Name": [
"1"
@ -230,9 +230,9 @@
50,
52,
48,
52,
55,
49,
48,
56,
46,
48,
49,
@ -358,7 +358,7 @@
"application/json; charset\u003dUTF-8"
],
"date": [
"Wed, 10 Apr 2024 17:31:13 GMT"
"Thu, 18 Jul 2024 17:47:36 GMT"
],
"server": [
"scaffolding on HTTPServer2"
@ -378,7 +378,7 @@
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgt0bUkyZ2JjU0JSWSjhmduwBjIOCgJGUhIIEgQSAgsMIAs%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240410.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xd29c85f6f8bbc897\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,23946420,23966208,23983296,23986028,23998056,24004644,24036948,24077241,24080738,24108448,24120819,24135310,24166867,24173287,24181174,24187377,24241378,24290971,24377598,24439361,24451319,24453989,24468724,24506784,24515423,24524098,24524562,24525810,24542367,24548627,24548629,24550458,24560416,24566687,24697069,24699899,39325854,39325978,51003636,51006181,51009781,51010235,51012659,51016856,51017346,51019626,51020570,51025415,51026715,51027870,51030450,51033399,51033765,51038805,51039200,51048489,51049131,51050361,51053689,51054765,51057501,51059572,51060353,51063643,51064835,51069269,51072748,51074183,51079239,51080343,51089441,51089956,51091331,51092661,51093434,51095478,51096577,51096646,51098297,51098299,51100401,51101454,51101980,51103858,51105868,51106995,51108006,51109078,51111738,51114094,51115184,51116067,51118932,51119507,51119509,51119511,51120529,51120722,51124104,51124221,51127505,51128585,51129216,51129218,51129220,51129222,51129224,51129395,51132393,51133103,51135346,51136218,51136785,51139379,51140749,51140806,51142840,51142842,51143076,51144925,51145052,51145216,51145749,51145822,51146568,51146962,51147119,51147558,51147788,51147896,51148750,51149480,51152043,51152050,51152207,51152530,51153775,51154012,51156581,51157411,51157729,51157841,51158470,51159917,51161142\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240410\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMInOXmvZa4hQMVyVBPBB3VtwTNMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCYJ61XIK64sp6ZFFS8sctxw\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgt2Rm1wQklIYlhmVSi4quW0BjIiCgJGUhIcEhgSFgsMDg8QERITFBUWFxgZGhscHR4fICEgUQ%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240718.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x5b4ab0a81ff46292\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"9479112,23804281,23946420,23966208,23986033,23998056,24004644,24077241,24135942,24166867,24181174,24241378,24290971,24299873,24425061,24439361,24453989,24456089,24468724,24542367,24548627,24548629,24550458,24566687,24699899,39325854,39326848,39326916,51009781,51010235,51016856,51017346,51020570,51025415,51030103,51037342,51037353,51041512,51043554,51043998,51050361,51053689,51057844,51057851,51057863,51060353,51063643,51064835,51072748,51091058,51091331,51095478,51098297,51098299,51101170,51102409,51105628,51111738,51113658,51113661,51115184,51116067,51117318,51118932,51120721,51124104,51133103,51139379,51141472,51148037,51148688,51148978,51148981,51149607,51152050,51157411,51157430,51157432,51157838,51158470,51158514,51160545,51162170,51163637,51165467,51165568,51170247,51172670,51172686,51172693,51172700,51172709,51172712,51172719,51172726,51175606,51176511,51177818,51178297,51178310,51178344,51178357,51178705,51178982,51181298,51182274,51183506,51183910,51184022,51184990,51186528,51187251,51189826,51190059,51190071,51190080,51190089,51190202,51190213,51190218,51190229,51190652,51191448,51195231,51196769,51197687,51197692,51197701,51197708,51199193,51200253,51200260,51200293,51200298,51200568,51201350,51201365,51201370,51201383,51201426,51201433,51201442,51201449,51201814,51203112,51204329,51204587,51207182,51207189,51207204,51207207,51211461,51212084,51212458,51212466,51212551,51212569,51213807,51217236,51217504,51219800,51219963,51221011,51221152,51222972,51223961,51224747,51224921,51225437,51226344,51227637,51227774,51227881,51228202,51228350,51228352,51228695,51228771,51228778,51228781,51228800,51228807,51228812,51230122,51230477,51230492,51231131,51231220,51232109,51232143,51232189,51232710,51234853,51235174,51235462,51237540,51237841,51238399,51238569,51238736,51240880,51240886,51241028,51241636,51241644,51242269\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240718\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMI3bWR5ZKxhwMVDEFPBB2x_wAnMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCYJ61XIK64sp6ZFFS8sctxw\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse"
}
}

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:30:55 GMT"
"Thu, 18 Jul 2024 17:49:51 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:30:55 GMT"
"Thu, 18 Jul 2024 17:49:51 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dZwoT5Z7xpsc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:30:55 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dc9wErMbEWpQ; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:49:51 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:13 GMT"
"Thu, 18 Jul 2024 17:48:26 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:13 GMT"
"Thu, 18 Jul 2024 17:48:26 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dLS28t4xIaN8; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:13 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dCZ8xidUhzf4; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:48:26 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -3,10 +3,10 @@
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Accept-Language": [
@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:16 GMT"
"Thu, 18 Jul 2024 17:50:19 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:16 GMT"
"Thu, 18 Jul 2024 17:50:19 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dwSKoU71ODdc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003d_U6D2v9w8QE; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:50:19 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -3,17 +3,17 @@
"httpMethod": "POST",
"url": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Cookie": [
"SOCS\u003dCAE\u003d"
],
"X-YouTube-Client-Version": [
"2.20240410.01.00"
"2.20240718.01.00"
],
"X-YouTube-Client-Name": [
"1"
@ -230,9 +230,9 @@
50,
52,
48,
52,
55,
49,
48,
56,
46,
48,
49,
@ -363,7 +363,7 @@
"application/json; charset\u003dUTF-8"
],
"date": [
"Wed, 10 Apr 2024 17:31:17 GMT"
"Thu, 18 Jul 2024 17:50:21 GMT"
],
"server": [
"scaffolding on HTTPServer2"
@ -383,7 +383,7 @@
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtlaWJlcDd6Q08yWSjlmduwBjIOCgJGUhIIEgQSAgsMIG4%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240410.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xce25bd397979378d\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,23946420,23966208,23971175,23983296,23986015,23998056,24004644,24036947,24077241,24080738,24120820,24135310,24166867,24181174,24187377,24241378,24290971,24377598,24439361,24451319,24453989,24468724,24506784,24515423,24524098,24524562,24542367,24548627,24548629,24550458,24560416,24566687,24697068,24699899,39325854,39325978,51003636,51006181,51009781,51010235,51012659,51016856,51017346,51019626,51020570,51025415,51026715,51027870,51033399,51033765,51038805,51039200,51048489,51050361,51053689,51057501,51059571,51060353,51063643,51064835,51069269,51072748,51074183,51079239,51080342,51089441,51089956,51091331,51092661,51095478,51096576,51096646,51098297,51098299,51101454,51103044,51103858,51105868,51106291,51106995,51108006,51109078,51111738,51115184,51116067,51117318,51118932,51119507,51119509,51119511,51124104,51126245,51127344,51128585,51129178,51129216,51129218,51129220,51129222,51129224,51129395,51129415,51129850,51132393,51133103,51135346,51136218,51136785,51139379,51140749,51140806,51142840,51142842,51144447,51144926,51145052,51145216,51145749,51146125,51146285,51146567,51146961,51146962,51147554,51147789,51147896,51148749,51149480,51152043,51152050,51152207,51152531,51153775,51154012,51156032,51156582,51157411,51157841,51157897,51158470,51158514,51158963,51159916\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240410\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIkPTVv5a4hQMVeYGxAx2EFQ5uMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCeY0bbntWzzVIaj2z3QigXg\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgt2UFRzSmhuM0J2SSjdq-W0BjIiCgJGUhIcEhgSFgsMDg8QERITFBUWFxgZGhscHR4fICEgVw%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20240718.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x93cf961e95e44789\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,23946420,23966208,23998056,24004644,24017848,24077241,24108447,24166867,24181174,24241378,24290971,24439361,24453989,24456089,24468724,24499533,24534951,24542367,24548627,24548629,24550458,24566687,24699899,39325854,39326848,39326916,51009781,51010235,51016856,51017346,51020570,51025415,51041512,51043555,51050361,51053689,51060353,51063643,51064835,51072748,51091058,51091331,51095478,51098297,51098299,51101170,51102410,51111738,51115184,51116067,51118932,51119595,51124104,51133103,51139379,51141472,51146015,51148688,51149607,51152050,51155997,51157411,51157430,51157432,51157838,51158470,51158514,51160545,51162170,51165467,51165568,51170249,51175606,51176511,51178705,51178982,51183909,51184022,51184990,51185141,51186528,51189826,51190652,51195231,51196180,51196769,51197711,51199193,51200569,51201814,51204329,51204585,51210036,51211461,51213715,51213807,51217504,51219800,51221011,51223961,51224135,51224747,51224921,51225437,51226344,51227881,51228202,51228350,51228352,51228695,51230477,51230492,51232143,51232684,51235463,51236950,51237540,51238568,51238736,51239209,51239303,51241029\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20240718\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIzZ65s5OxhwMVE0RPBB33uQzmMghleHRlcm5hbJoBAA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCeY0bbntWzzVIaj2z3QigXg\",\"params\":\"EgC4AQCSAwDyBgQKAjIA\"}}}",
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?prettyPrint\u003dfalse"
}
}

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:14 GMT"
"Thu, 18 Jul 2024 17:45:17 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:14 GMT"
"Thu, 18 Jul 2024 17:45:17 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dWd13sWd3cpk; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:14 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dmt8ojN0j5ss; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:45:17 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -13,7 +13,7 @@
"SOCS\u003dCAE\u003d"
],
"X-YouTube-Client-Version": [
"2.20240410.01.00"
"2.20240718.01.00"
],
"X-YouTube-Client-Name": [
"1"
@ -256,9 +256,9 @@
50,
52,
48,
52,
55,
49,
48,
56,
46,
48,
49,
@ -376,7 +376,7 @@
"application/json; charset\u003dUTF-8"
],
"date": [
"Wed, 10 Apr 2024 17:31:15 GMT"
"Thu, 18 Jul 2024 17:45:19 GMT"
],
"server": [
"scaffolding on HTTPServer2"

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:15 GMT"
"Thu, 18 Jul 2024 17:49:01 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:15 GMT"
"Thu, 18 Jul 2024 17:49:01 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dTN_tYcQa7Ro; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:15 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dqO-eHy3Kz4k; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:49:01 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -3,10 +3,10 @@
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"Origin": [
"https://www.youtube.com"
],
"Origin": [
"Referer": [
"https://www.youtube.com"
],
"Accept-Language": [
@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:15 GMT"
"Thu, 18 Jul 2024 17:46:52 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:15 GMT"
"Thu, 18 Jul 2024 17:46:52 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dLEBE5JO8Blk; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:15 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dCPnrnhg5fno; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:46:52 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:18 GMT"
"Thu, 18 Jul 2024 17:52:03 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:18 GMT"
"Thu, 18 Jul 2024 17:52:03 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003ddxRIN7KwkLY; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:18 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dCy4aX6DBam4; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:52:03 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -41,19 +41,19 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Wed, 10 Apr 2024 17:31:18 GMT"
"Thu, 18 Jul 2024 17:51:50 GMT"
],
"expires": [
"Wed, 10 Apr 2024 17:31:18 GMT"
"Thu, 18 Jul 2024 17:51:50 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
"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-factor\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
"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\"}]}"
@ -62,8 +62,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dgkaI3HruKqg; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 15-Jul-2021 17:31:18 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dgoEUVn3I_Gk; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 22-Oct-2021 17:51:50 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

Some files were not shown because too many files have changed in this diff Show More