Merge pull request #1277 from litetex/fix-tests

Fix tests and cleanup other stuff
This commit is contained in:
Stypox 2025-02-15 15:23:27 +01:00 committed by GitHub
commit b1ecb6882f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
278 changed files with 60682 additions and 17416 deletions

View File

@ -29,8 +29,6 @@ dependencies {
implementation 'org.jsoup:jsoup:1.17.2'
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
// 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.15'
checkstyle "com.puppycrawl.tools:checkstyle:$checkstyleVersion"
@ -40,6 +38,6 @@ dependencies {
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation "com.squareup.okhttp3:okhttp:3.12.13"
testImplementation 'com.google.code.gson:gson:2.11.0'
testImplementation "com.squareup.okhttp3:okhttp:4.12.0"
testImplementation 'com.google.code.gson:gson:2.12.1'
}

View File

@ -13,11 +13,12 @@ import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
public final class PlaylistInfo extends ListInfo<StreamInfoItem> {
/**
@ -45,7 +46,11 @@ public final class PlaylistInfo extends ListInfo<StreamInfoItem> {
/**
* A mix made only of streams from (or related to) the same channel, for example YouTube
* channel mixes
*
* @deprecated There is currently no service that implements this.
* YouTube removed all playlists with this type around 2024-06
*/
@Deprecated
MIX_CHANNEL,
/**

View File

@ -48,6 +48,7 @@ public class BandcampSearchExtractor extends SearchExtractor {
return Collections.emptyList();
}
@Override
public InfoItemsPage<InfoItem> getPage(final Page page)
throws IOException, ExtractionException {
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
@ -97,16 +98,12 @@ public class BandcampSearchExtractor extends SearchExtractor {
}
}
// Search results appear to be capped at six pages
assert pages.size() < 10;
String nextUrl = null;
if (currentPage < pages.size()) {
nextUrl = page.getUrl().substring(0, page.getUrl().length() - 1) + (currentPage + 1);
}
return new InfoItemsPage<>(collector, new Page(nextUrl));
}
@Nonnull

View File

@ -7,22 +7,24 @@ import static org.schabi.newpipe.extractor.services.bandcamp.extractors.Bandcamp
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class BandcampSuggestionExtractor extends SuggestionExtractor {
private static final String AUTOCOMPLETE_URL = BASE_API_URL + "/fuzzysearch/1/autocomplete?q=";
private static final String AUTOCOMPLETE_URL = BASE_API_URL
+ "/bcsearch_public_api/1/autocomplete_elastic";
public BandcampSuggestionExtractor(final StreamingService service) {
super(service);
}
@ -33,7 +35,18 @@ public class BandcampSuggestionExtractor extends SuggestionExtractor {
try {
final JsonObject fuzzyResults = JsonParser.object().from(downloader
.get(AUTOCOMPLETE_URL + Utils.encodeUrlUtf8(query)).responseBody());
.postWithContentTypeJson(
AUTOCOMPLETE_URL,
Collections.emptyMap(),
JsonWriter.string()
.object()
.value("fan_id", (String) null)
.value("full_page", false)
.value("search_filter", "")
.value("search_text", query)
.end()
.done()
.getBytes(StandardCharsets.UTF_8)).responseBody());
return fuzzyResults.getObject("auto").getArray("results").stream()
.filter(JsonObject.class::isInstance)

View File

@ -60,7 +60,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.ProtoBuilder;
import org.schabi.newpipe.extractor.utils.RandomStringFromAlphabetGenerator;
import org.schabi.newpipe.extractor.utils.Utils;
@ -235,23 +234,6 @@ public final class YoutubeParsingHelper {
return url.getHost().equalsIgnoreCase("y2u.be");
}
public static String randomVisitorData(final ContentCountry country) {
final ProtoBuilder pbE2 = new ProtoBuilder();
pbE2.string(2, "");
pbE2.varint(4, numberGenerator.nextInt(255) + 1);
final ProtoBuilder pbE = new ProtoBuilder();
pbE.string(1, country.getCountryCode());
pbE.bytes(2, pbE2.toBytes());
final ProtoBuilder pb = new ProtoBuilder();
pb.string(1, RandomStringFromAlphabetGenerator.generate(
CONTENT_PLAYBACK_NONCE_ALPHABET, 11, numberGenerator));
pb.varint(5, System.currentTimeMillis() / 1000 - numberGenerator.nextInt(600000));
pb.bytes(6, pbE.toBytes());
return pb.toUrlencodedBase64();
}
/**
* Parses the duration string of the video expecting ":" or "." as separators
*
@ -359,16 +341,6 @@ public final class YoutubeParsingHelper {
return playlistId.startsWith("RDAMVM") || playlistId.startsWith("RDCLAK");
}
/**
* Checks if the given playlist id is a YouTube Channel Mix (auto-generated playlist)
* Ids from a YouTube channel Mix start with "RDCM"
*
* @return Whether given id belongs to a YouTube Channel Mix
*/
public static boolean isYoutubeChannelMixId(@Nonnull final String playlistId) {
return playlistId.startsWith("RDCM");
}
/**
* Checks if the given playlist id is a YouTube Genre Mix (auto-generated playlist)
* Ids from a YouTube Genre Mix start with "RDGMEM"
@ -399,11 +371,6 @@ public final class YoutubeParsingHelper {
} else if (isYoutubeMusicMixId(playlistId)) {
return playlistId.substring(6);
} else if (isYoutubeChannelMixId(playlistId)) {
// Channel mixes are of the form RMCM{channelId}, so videoId can't be determined
throw new ParsingException("Video id could not be determined from channel mix id: "
+ playlistId);
} else if (isYoutubeGenreMixId(playlistId)) {
// Genre mixes are of the form RDGMEM{garbage}, so videoId can't be determined
throw new ParsingException("Video id could not be determined from genre mix id: "
@ -438,8 +405,6 @@ public final class YoutubeParsingHelper {
throw new ParsingException("Could not extract playlist type from empty playlist id");
} else if (isYoutubeMusicMixId(playlistId)) {
return PlaylistInfo.PlaylistType.MIX_MUSIC;
} else if (isYoutubeChannelMixId(playlistId)) {
return PlaylistInfo.PlaylistType.MIX_CHANNEL;
} else if (isYoutubeGenreMixId(playlistId)) {
return PlaylistInfo.PlaylistType.MIX_GENRE;
} else if (isYoutubeMixId(playlistId)) { // normal mix

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.extractor.services.youtube.extractors;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
@ -61,10 +62,16 @@ public class YoutubeMusicArtistInfoItemExtractor implements ChannelInfoItemExtra
@Override
public long getSubscriberCount() throws ParsingException {
final String subscriberCount = getTextFromObject(artistInfoItem.getArray("flexColumns")
.getObject(2)
final JsonArray flexColumns = artistInfoItem.getArray("flexColumns");
final JsonArray runs = flexColumns
.getObject(flexColumns.size() - 1)
.getObject("musicResponsiveListItemFlexColumnRenderer")
.getObject("text"));
.getObject("text")
.getArray("runs");
// NOTE: YoutubeParsingHelper#getTextFromObject would use all entries from the run array,
// which is not wanted as only the last entry contains the actual subscriberCount
final String subscriberCount = runs.getObject(runs.size() - 1)
.getString("text");
if (!isNullOrEmpty(subscriberCount)) {
try {
return Utils.mixedNumberWordToLong(subscriberCount);

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
@ -57,14 +56,6 @@ public final class YoutubePlaylistLinkHandlerFactory extends ListLinkHandlerFact
"the list-ID given in the URL does not match the list pattern");
}
if (YoutubeParsingHelper.isYoutubeChannelMixId(listID)
&& Utils.getQueryValue(urlObj, "v") == null) {
// Video id can't be determined from the channel mix id.
// See YoutubeParsingHelper#extractVideoIdFromMixId
throw new ContentNotSupportedException(
"Channel Mix without a video id are not supported");
}
return listID;
} catch (final Exception exception) {
throw new ParsingException("Error could not parse URL: " + exception.getMessage(),

View File

@ -114,7 +114,7 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
try {
url = Utils.stringToURL(urlString);
} catch (final MalformedURLException e) {
throw new IllegalArgumentException("The given URL is not valid");
throw new ParsingException("The given URL is not valid", e);
}
final String host = url.getHost();

View File

@ -1,73 +0,0 @@
package org.schabi.newpipe.extractor.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ProtoBuilder {
ByteArrayOutputStream byteBuffer;
public ProtoBuilder() {
this.byteBuffer = new ByteArrayOutputStream();
}
public byte[] toBytes() {
return byteBuffer.toByteArray();
}
public String toUrlencodedBase64() {
final String b64 = Base64.getUrlEncoder().encodeToString(toBytes());
return URLEncoder.encode(b64, StandardCharsets.UTF_8);
}
private void writeVarint(final long val) {
try {
if (val == 0) {
byteBuffer.write(new byte[]{(byte) 0});
} else {
long v = val;
while (v != 0) {
byte b = (byte) (v & 0x7f);
v >>= 7;
if (v != 0) {
b |= (byte) 0x80;
}
byteBuffer.write(new byte[]{b});
}
}
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
private void field(final int field, final byte wire) {
final long fbits = ((long) field) << 3;
final long wbits = ((long) wire) & 0x07;
final long val = fbits | wbits;
writeVarint(val);
}
public void varint(final int field, final long val) {
field(field, (byte) 0);
writeVarint(val);
}
public void string(final int field, final String string) {
final byte[] strBts = string.getBytes(StandardCharsets.UTF_8);
bytes(field, strBts);
}
public void bytes(final int field, final byte[] bytes) {
field(field, (byte) 2);
writeVarint(bytes.length);
try {
byteBuffer.write(bytes);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
}

View File

@ -36,7 +36,7 @@ public class DownloaderFactory {
* @param path The path to the folder where mocks are saved/retrieved.
* Preferably starting with {@link DownloaderFactory#RESOURCE_PATH}
*/
public static Downloader getDownloader(final String path) throws IOException {
public static Downloader getDownloader(final String path) {
final DownloaderType type = getDownloaderType();
switch (type) {
case REAL:

View File

@ -1,11 +1,13 @@
package org.schabi.newpipe.downloader;
import org.schabi.newpipe.downloader.ratelimiting.RateLimitedClientWrapper;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -13,6 +15,7 @@ import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
@ -21,13 +24,19 @@ public final class DownloaderTestImpl extends Downloader {
/**
* Should be the latest Firefox ESR version.
*/
private static final String USER_AGENT
= "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
private static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
private static DownloaderTestImpl instance;
private final OkHttpClient client;
private final RateLimitedClientWrapper clientWrapper;
private DownloaderTestImpl(final OkHttpClient.Builder builder) {
this.client = builder.readTimeout(30, TimeUnit.SECONDS).build();
this.clientWrapper = new RateLimitedClientWrapper(builder
.readTimeout(30, TimeUnit.SECONDS)
// Required for certain services
// For example Bandcamp otherwise fails on Windows with Java 17+
// as their Fastly-CDN returns 403
.connectionSpecs(Arrays.asList(ConnectionSpec.RESTRICTED_TLS))
.build());
}
/**
@ -59,45 +68,40 @@ public final class DownloaderTestImpl extends Downloader {
RequestBody requestBody = null;
if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend);
requestBody = RequestBody.create(dataToSend);
}
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)
.method(httpMethod, requestBody)
.url(url)
.addHeader("User-Agent", USER_AGENT);
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
final String headerName = pair.getKey();
final List<String> headerValueList = pair.getValue();
if (headerValueList.size() > 1) {
headers.forEach((headerName, headerValueList) -> {
requestBuilder.removeHeader(headerName);
for (String headerValue : headerValueList) {
requestBuilder.addHeader(headerName, headerValue);
}
} else if (headerValueList.size() == 1) {
requestBuilder.header(headerName, headerValueList.get(0));
}
}
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
headerValueList.forEach(headerValue ->
requestBuilder.addHeader(headerName, headerValue));
});
try (okhttp3.Response response =
clientWrapper.executeRequestWithLimit(requestBuilder.build())
) {
if (response.code() == 429) {
response.close();
throw new ReCaptchaException("reCaptcha Challenge requested", url);
}
final ResponseBody body = response.body();
String responseBodyToReturn = null;
try (ResponseBody body = response.body()) {
if (body != null) {
responseBodyToReturn = body.string();
}
}
final String latestUrl = response.request().url().toString();
return new Response(response.code(), response.message(), response.headers().toMultimap(),
responseBodyToReturn, latestUrl);
return new Response(
response.code(),
response.message(),
response.headers().toMultimap(),
responseBodyToReturn,
response.request().url().toString());
}
}
}

View File

@ -10,6 +10,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@ -26,19 +27,22 @@ class MockDownloader extends Downloader {
private final String path;
private final Map<Request, Response> mocks;
public MockDownloader(@Nonnull final String path) throws IOException {
public MockDownloader(@Nonnull final String path) {
this.path = path;
this.mocks = new HashMap<>();
final File[] files = new File(path).listFiles();
if (files != null) {
for (final File file : files) {
if (file.getName().startsWith(RecordingDownloader.FILE_NAME_PREFIX)) {
final InputStreamReader reader = new InputStreamReader(new FileInputStream(
file), StandardCharsets.UTF_8);
final TestRequestResponse response = new GsonBuilder()
final TestRequestResponse response;
try(final InputStreamReader reader = new InputStreamReader(
new FileInputStream(file), StandardCharsets.UTF_8)) {
response = new GsonBuilder()
.create()
.fromJson(reader, TestRequestResponse.class);
reader.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
mocks.put(response.getRequest(), response.getResponse());
}
}

View File

@ -11,11 +11,11 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Random;
import javax.annotation.Nonnull;
@ -47,26 +47,12 @@ class RecordingDownloader extends Downloader {
private int index = 0;
private final String path;
// try to prevent ReCaptchaExceptions / rate limits by tracking and throttling the requests
/**
* The maximum number of requests per 20 seconds which are executed
* by the {@link RecordingDownloader}.
* 20 seconds is used as upper bound because the rate limit can be triggered within 30 seconds
* and hitting the rate limit should be prevented because it comes with a bigger delay.
* The values can be adjusted when executing the downloader and running into problems.
* <p>TODO: Allow adjusting the value by setting a param in the gradle command</p>
*/
private static final int MAX_REQUESTS_PER_20_SECONDS = 30;
private static final long[] requestTimes = new long[MAX_REQUESTS_PER_20_SECONDS];
private static int requestTimesCursor = -1;
private static final Random throttleRandom = new Random();
/**
* Creates the folder described by {@code stringPath} if it does not exist.
* Deletes existing files starting with {@link RecordingDownloader#FILE_NAME_PREFIX}.
* @param stringPath Path to the folder where the json files will be saved to.
*/
public RecordingDownloader(final String stringPath) throws IOException {
public RecordingDownloader(final String stringPath) {
this.path = stringPath;
final Path path = Paths.get(stringPath);
final File folder = path.toFile();
@ -77,78 +63,42 @@ class RecordingDownloader extends Downloader {
}
}
} else {
try {
Files.createDirectories(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
@Override
public Response execute(@Nonnull final Request request) throws IOException,
ReCaptchaException {
// Delay the execution if the max number of requests per minute is reached
final long currentTime = System.currentTimeMillis();
// the cursor points to the latest request time and the next position is the oldest one
final int oldestRequestTimeCursor = (requestTimesCursor + 1) % requestTimes.length;
final long oldestRequestTime = requestTimes[oldestRequestTimeCursor];
if (oldestRequestTime + 20_000 >= currentTime) {
try {
// sleep at least until the oldest request is 20s old, but not more than 20s
final int minSleepTime = (int) (currentTime - oldestRequestTime);
Thread.sleep(minSleepTime + throttleRandom.nextInt(20_000 - minSleepTime));
} catch (InterruptedException e) {
// handle the exception gracefully because it's not critical for the test
System.err.println("Error while throttling the RecordingDownloader.");
e.printStackTrace();
}
}
requestTimesCursor = oldestRequestTimeCursor; // the oldest value needs to be overridden
requestTimes[requestTimesCursor] = System.currentTimeMillis();
// Handle ReCaptchaExceptions by retrying the request once after a while
try {
return executeRequest(request);
} catch (ReCaptchaException e) {
try {
// sleep for 35-60 seconds to circumvent the rate limit
System.out.println("Throttling the RecordingDownloader to handle a ReCaptcha."
+ " Sleeping for 35-60 seconds.");
Thread.sleep(35_000 + throttleRandom.nextInt(25_000));
} catch (InterruptedException ie) {
// handle the exception gracefully because it's not critical for the test
System.err.println("Error while throttling the RecordingDownloader.");
ie.printStackTrace();
e.printStackTrace();
}
return executeRequest(request);
}
}
@Nonnull
private Response executeRequest(@Nonnull final Request request) throws IOException,
ReCaptchaException {
final Downloader downloader = DownloaderTestImpl.getInstance();
Response response = downloader.execute(request);
String cleanedResponseBody = response.responseBody().replaceAll(IP_V4_PATTERN, "127.0.0.1");
response = new Response(
response.responseCode(),
response.responseMessage(),
response.responseHeaders(),
cleanedResponseBody,
response.responseBody().replaceAll(IP_V4_PATTERN, "127.0.0.1"),
response.latestUrl()
);
final File outputFile = new File(path + File.separator + FILE_NAME_PREFIX + index
+ ".json");
final File outputFile = new File(
path + File.separator + FILE_NAME_PREFIX + index + ".json");
index++;
outputFile.createNewFile();
final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(outputFile),
StandardCharsets.UTF_8);
try (final OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(outputFile), StandardCharsets.UTF_8)) {
new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(new TestRequestResponse(request, response), writer);
writer.flush();
writer.close();
}
return response;
}

View File

@ -0,0 +1,84 @@
package org.schabi.newpipe.downloader.ratelimiting;
import org.schabi.newpipe.downloader.ratelimiting.limiter.RateLimiter;
import java.io.IOException;
import java.net.ProtocolException;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Predicate;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class RateLimitedClientWrapper {
private static final boolean DEBUG_PRINT =
"1".equals(System.getProperty("rateLimitClientDebugPrint",
System.getenv("RATE_LIMIT_CLIENT_DEBUG_PRINT")));
private static final int REQUEST_RATE_LIMITED_WAIT_MS = 5_000;
private static final Map<Predicate<String>, RateLimiter> FORCED_RATE_LIMITERS = Map.ofEntries(
Map.entry(host -> host.endsWith("youtube.com"),
RateLimiter.create(1.6, Duration.ofSeconds(1))),
Map.entry(host -> host.endsWith("bandcamp.com"),
RateLimiter.create(2.5, Duration.ofSeconds(1)))
);
private final OkHttpClient client;
private final Map<String, RateLimiter> hostRateLimiters = new LinkedHashMap<>();
public RateLimitedClientWrapper(final OkHttpClient client) {
this.client = client;
}
protected RateLimiter getRateLimiterFor(final Request request) {
return hostRateLimiters.computeIfAbsent(request.url().host(), host ->
FORCED_RATE_LIMITERS.entrySet()
.stream()
.filter(e -> e.getKey().test(host))
.findFirst()
.map(Map.Entry::getValue)
.orElseGet(() ->
// Default rate limiter per domain
RateLimiter.create(5, Duration.ofSeconds(3))));
}
public Response executeRequestWithLimit(final Request request) throws IOException {
Exception cause = null;
for (int tries = 1; tries <= 3; tries++) {
try {
final double rateLimitedSec = getRateLimiterFor(request).acquire();
if (DEBUG_PRINT) {
System.out.println(
"[RATE-LIMIT] Waited " + rateLimitedSec + "s for " + request.url());
}
final Response response = client.newCall(request).execute();
if(response.code() != 429) { // 429 = Too many requests
return response;
}
cause = new IllegalStateException("HTTP 429 - Too many requests");
} catch (final ProtocolException pre) {
if (!pre.getMessage().startsWith("Too many follow-up")) { // -> Too many requests
throw pre;
}
cause = pre;
}
final int waitMs = REQUEST_RATE_LIMITED_WAIT_MS * tries;
if (DEBUG_PRINT) {
System.out.println(
"[TOO-MANY-REQUESTS] Waiting " + waitMs + "ms for " + request.url());
}
try {
Thread.sleep(waitMs);
} catch (final InterruptedException iex) {
Thread.currentThread().interrupt();
}
}
throw new IllegalStateException(
"Retrying/Rate-limiting for " + request.url() + " failed", cause);
}
}

View File

@ -0,0 +1,497 @@
/*
* Copyright (C) 2012 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.schabi.newpipe.downloader.ratelimiting.limiter;
import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.schabi.newpipe.downloader.ratelimiting.limiter.SmoothRateLimiter.SmoothWarmingUp;
import java.time.Duration;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* A rate limiter. Conceptually, a rate limiter distributes permits at a configurable rate. Each
* {@link #acquire()} blocks if necessary until a permit is available, and then takes it. Once
* acquired, permits need not be released.
*
* <p>{@code RateLimiter} is safe for concurrent use: It will restrict the total rate of calls from
* all threads. Note, however, that it does not guarantee fairness.
*
* <p>Rate limiters are often used to restrict the rate at which some physical or logical resource
* is accessed. This is in contrast to {@link java.util.concurrent.Semaphore} which restricts the
* number of concurrent accesses instead of the rate (note though that concurrency and rate are
* closely related, e.g. see <a href="http://en.wikipedia.org/wiki/Little%27s_law">Little's
* Law</a>).
*
* <p>A {@code RateLimiter} is defined primarily by the rate at which permits are issued. Absent
* additional configuration, permits will be distributed at a fixed rate, defined in terms of
* permits per second. Permits will be distributed smoothly, with the delay between individual
* permits being adjusted to ensure that the configured rate is maintained.
*
* <p>It is possible to configure a {@code RateLimiter} to have a warmup period during which time
* the permits issued each second steadily increases until it hits the stable rate.
*
* <p>As an example, imagine that we have a list of tasks to execute, but we don't want to submit
* more than 2 per second:
*
* <pre>{@code
* final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is "2 permits per second"
* void submitTasks(List<Runnable> tasks, Executor executor) {
* for (Runnable task : tasks) {
* rateLimiter.acquire(); // may wait
* executor.execute(task);
* }
* }
* }</pre>
*
* <p>As another example, imagine that we produce a stream of data, and we want to cap it at 5kb per
* second. This could be accomplished by requiring a permit per byte, and specifying a rate of 5000
* permits per second:
*
* <pre>{@code
* final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second
* void submitPacket(byte[] packet) {
* rateLimiter.acquire(packet.length);
* networkService.send(packet);
* }
* }</pre>
*
* <p>It is important to note that the number of permits requested <i>never</i> affects the
* throttling of the request itself (an invocation to {@code acquire(1)} and an invocation to {@code
* acquire(1000)} will result in exactly the same throttling, if any), but it affects the throttling
* of the <i>next</i> request. I.e., if an expensive task arrives at an idle RateLimiter, it will be
* granted immediately, but it is the <i>next</i> request that will experience extra throttling,
* thus paying for the cost of the expensive task.
*
* @author Dimitris Andreou
* @since 13.0
*/
public abstract class RateLimiter {
public static final double DEFAULT_COLD_FACTOR = 3.0;
/**
* Creates a {@code RateLimiter} with the specified stable throughput, given as "permits per
* second" (commonly referred to as <i>QPS</i>, queries per second), and a <i>warmup period</i>,
* during which the {@code RateLimiter} smoothly ramps up its rate, until it reaches its maximum
* rate at the end of the period (as long as there are enough requests to saturate it). Similarly,
* if the {@code RateLimiter} is left <i>unused</i> for a duration of {@code warmupPeriod}, it
* will gradually return to its "cold" state, i.e. it will go through the same warming up process
* as when it was first created.
*
* <p>The returned {@code RateLimiter} is intended for cases where the resource that actually
* fulfills the requests (e.g., a remote server) needs "warmup" time, rather than being
* immediately accessed at the stable (maximum) rate.
*
* <p>The returned {@code RateLimiter} starts in a "cold" state (i.e. the warmup period will
* follow), and if it is left unused for long enough, it will return to that state.
*
* @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in how many
* permits become available per second
* @param warmupPeriod the duration of the period where the {@code RateLimiter} ramps up its rate,
* before reaching its stable (maximum) rate
* @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero or {@code
* warmupPeriod} is negative
* @since 28.0 (but only since 33.4.0 in the Android flavor)
*/
public static RateLimiter create(
final double permitsPerSecond,
final Duration warmupPeriod
) {
return create(permitsPerSecond, warmupPeriod, DEFAULT_COLD_FACTOR);
}
/**
* Creates a {@code RateLimiter} with the specified stable throughput, given as "permits per
* second" (commonly referred to as <i>QPS</i>, queries per second), and a <i>warmup period</i>,
* during which the {@code RateLimiter} smoothly ramps up its rate, until it reaches its maximum
* rate at the end of the period (as long as there are enough requests to saturate it). Similarly,
* if the {@code RateLimiter} is left <i>unused</i> for a duration of {@code warmupPeriod}, it
* will gradually return to its "cold" state, i.e. it will go through the same warming up process
* as when it was first created.
*
* <p>The returned {@code RateLimiter} is intended for cases where the resource that actually
* fulfills the requests (e.g., a remote server) needs "warmup" time, rather than being
* immediately accessed at the stable (maximum) rate.
*
* <p>The returned {@code RateLimiter} starts in a "cold" state (i.e. the warmup period will
* follow), and if it is left unused for long enough, it will return to that state.
*
* @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in how many
* permits become available per second
* @param warmupPeriod the duration of the period where the {@code RateLimiter} ramps up its rate,
* before reaching its stable (maximum) rate
* @param coldFactor see {@link SmoothWarmingUp}
* @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero or {@code
* warmupPeriod} is negative
* @since 28.0 (but only since 33.4.0 in the Android flavor)
*/
public static RateLimiter create(
final double permitsPerSecond,
final Duration warmupPeriod,
final double coldFactor
) {
return create(permitsPerSecond, warmupPeriod.toNanos(), TimeUnit.NANOSECONDS, coldFactor);
}
/**
* Creates a {@code RateLimiter} with the specified stable throughput, given as "permits per
* second" (commonly referred to as <i>QPS</i>, queries per second), and a <i>warmup period</i>,
* during which the {@code RateLimiter} smoothly ramps up its rate, until it reaches its maximum
* rate at the end of the period (as long as there are enough requests to saturate it). Similarly,
* if the {@code RateLimiter} is left <i>unused</i> for a duration of {@code warmupPeriod}, it
* will gradually return to its "cold" state, i.e. it will go through the same warming up process
* as when it was first created.
*
* <p>The returned {@code RateLimiter} is intended for cases where the resource that actually
* fulfills the requests (e.g., a remote server) needs "warmup" time, rather than being
* immediately accessed at the stable (maximum) rate.
*
* <p>The returned {@code RateLimiter} starts in a "cold" state (i.e. the warmup period will
* follow), and if it is left unused for long enough, it will return to that state.
*
* @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in how many
* permits become available per second
* @param warmupPeriod the duration of the period where the {@code RateLimiter} ramps up its rate,
* before reaching its stable (maximum) rate
* @param unit the time unit of the warmupPeriod argument
* @param coldFactor see {@link SmoothWarmingUp}
* @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero or {@code
* warmupPeriod} is negative
*/
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
public static RateLimiter create(
final double permitsPerSecond,
final long warmupPeriod,
final TimeUnit unit,
final double coldFactor
) {
if(warmupPeriod < 0) {
throw new IllegalArgumentException(
"warmupPeriod must not be negative: " + warmupPeriod);
}
return create(
permitsPerSecond, warmupPeriod, unit, coldFactor, SleepingStopwatch.createFromSystemTimer());
}
static RateLimiter create(
final double permitsPerSecond,
final long warmupPeriod,
final TimeUnit unit,
final double coldFactor,
final SleepingStopwatch stopwatch) {
final RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
/**
* The underlying timer; used both to measure elapsed time and sleep as necessary. A separate
* object to facilitate testing.
*/
private final SleepingStopwatch stopwatch;
// Can't be initialized in the constructor because mocks don't call the constructor.
private volatile Object mutexDoNotUseDirectly;
private Object mutex() {
Object mutex = mutexDoNotUseDirectly;
if (mutex == null) {
synchronized (this) {
mutex = mutexDoNotUseDirectly;
if (mutex == null) {
mutexDoNotUseDirectly = mutex = new Object();
}
}
}
return mutex;
}
RateLimiter(final SleepingStopwatch stopwatch) {
this.stopwatch = Objects.requireNonNull(stopwatch);
}
/**
* Updates the stable rate of this {@code RateLimiter}, that is, the {@code permitsPerSecond}
* argument provided in the factory method that constructed the {@code RateLimiter}. Currently
* throttled threads will <b>not</b> be awakened as a result of this invocation, thus they do not
* observe the new rate; only subsequent requests will.
*
* <p>Note though that, since each request repays (by waiting, if necessary) the cost of the
* <i>previous</i> request, this means that the very next request after an invocation to {@code
* setRate} will not be affected by the new rate; it will pay the cost of the previous request,
* which is in terms of the previous rate.
*
* <p>The behavior of the {@code RateLimiter} is not modified in any other way, e.g. if the {@code
* RateLimiter} was configured with a warmup period of 20 seconds, it still has a warmup period of
* 20 seconds after this method invocation.
*
* @param permitsPerSecond the new stable rate of this {@code RateLimiter}
* @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero
*/
public final void setRate(final double permitsPerSecond) {
if(permitsPerSecond <= 0.0) {
throw new IllegalArgumentException("rate must be positive");
}
synchronized (mutex()) {
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
abstract void doSetRate(double permitsPerSecond, long nowMicros);
/**
* Returns the stable rate (as {@code permits per seconds}) with which this {@code RateLimiter} is
* configured with. The initial value of this is the same as the {@code permitsPerSecond} argument
* passed in the factory method that produced this {@code RateLimiter}, and it is only updated
* after invocations to {@linkplain #setRate}.
*/
public final double getRate() {
synchronized (mutex()) {
return doGetRate();
}
}
abstract double doGetRate();
/**
* Acquires a single permit from this {@code RateLimiter}, blocking until the request can be
* granted. Tells the amount of time slept, if any.
*
* <p>This method is equivalent to {@code acquire(1)}.
*
* @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited
* @since 16.0 (present in 13.0 with {@code void} return type})
*/
@CanIgnoreReturnValue
public double acquire() {
return acquire(1);
}
/**
* Acquires the given number of permits from this {@code RateLimiter}, blocking until the request
* can be granted. Tells the amount of time slept, if any.
*
* @param permits the number of permits to acquire
* @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited
* @throws IllegalArgumentException if the requested number of permits is negative or zero
* @since 16.0 (present in 13.0 with {@code void} return type})
*/
@CanIgnoreReturnValue
public double acquire(final int permits) {
final long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
/**
* Reserves the given number of permits from this {@code RateLimiter} for future use, returning
* the number of microseconds until the reservation can be consumed.
*
* @return time in microseconds to wait until the resource can be acquired, never negative
*/
final long reserve(final int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
/**
* Acquires a permit from this {@code RateLimiter} if it can be obtained without exceeding the
* specified {@code timeout}, or returns {@code false} immediately (without waiting) if the permit
* would not have been granted before the timeout expired.
*
* <p>This method is equivalent to {@code tryAcquire(1, timeout)}.
*
* @param timeout the maximum time to wait for the permit. Negative values are treated as zero.
* @return {@code true} if the permit was acquired, {@code false} otherwise
* @throws IllegalArgumentException if the requested number of permits is negative or zero
* @since 28.0 (but only since 33.4.0 in the Android flavor)
*/
public boolean tryAcquire(final Duration timeout) {
return tryAcquire(1, timeout.toNanos(), TimeUnit.NANOSECONDS);
}
/**
* Acquires a permit from this {@code RateLimiter} if it can be obtained without exceeding the
* specified {@code timeout}, or returns {@code false} immediately (without waiting) if the permit
* would not have been granted before the timeout expired.
*
* <p>This method is equivalent to {@code tryAcquire(1, timeout, unit)}.
*
* @param timeout the maximum time to wait for the permit. Negative values are treated as zero.
* @param unit the time unit of the timeout argument
* @return {@code true} if the permit was acquired, {@code false} otherwise
* @throws IllegalArgumentException if the requested number of permits is negative or zero
*/
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
public boolean tryAcquire(final long timeout, final TimeUnit unit) {
return tryAcquire(1, timeout, unit);
}
/**
* Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay.
*
* <p>This method is equivalent to {@code tryAcquire(permits, 0, anyUnit)}.
*
* @param permits the number of permits to acquire
* @return {@code true} if the permits were acquired, {@code false} otherwise
* @throws IllegalArgumentException if the requested number of permits is negative or zero
* @since 14.0
*/
public boolean tryAcquire(final int permits) {
return tryAcquire(permits, 0, MICROSECONDS);
}
/**
* Acquires a permit from this {@link RateLimiter} if it can be acquired immediately without
* delay.
*
* <p>This method is equivalent to {@code tryAcquire(1)}.
*
* @return {@code true} if the permit was acquired, {@code false} otherwise
* @since 14.0
*/
public boolean tryAcquire() {
return tryAcquire(1, 0, MICROSECONDS);
}
/**
* Acquires the given number of permits from this {@code RateLimiter} if it can be obtained
* without exceeding the specified {@code timeout}, or returns {@code false} immediately (without
* waiting) if the permits would not have been granted before the timeout expired.
*
* @param permits the number of permits to acquire
* @param timeout the maximum time to wait for the permits. Negative values are treated as zero.
* @return {@code true} if the permits were acquired, {@code false} otherwise
* @throws IllegalArgumentException if the requested number of permits is negative or zero
* @since 28.0 (but only since 33.4.0 in the Android flavor)
*/
public boolean tryAcquire(final int permits, final Duration timeout) {
return tryAcquire(permits, timeout.toNanos(), TimeUnit.NANOSECONDS);
}
/**
* Acquires the given number of permits from this {@code RateLimiter} if it can be obtained
* without exceeding the specified {@code timeout}, or returns {@code false} immediately (without
* waiting) if the permits would not have been granted before the timeout expired.
*
* @param permits the number of permits to acquire
* @param timeout the maximum time to wait for the permits. Negative values are treated as zero.
* @param unit the time unit of the timeout argument
* @return {@code true} if the permits were acquired, {@code false} otherwise
* @throws IllegalArgumentException if the requested number of permits is negative or zero
*/
@SuppressWarnings("GoodTime") // should accept a java.time.Duration
public boolean tryAcquire(final int permits, final long timeout, final TimeUnit unit) {
final long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
final long microsToWait;
synchronized (mutex()) {
final long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
private boolean canAcquire(final long nowMicros, final long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
/**
* Reserves next ticket and returns the wait time that the caller must wait for.
*
* @return the required wait time, never negative
*/
final long reserveAndGetWaitLength(final int permits, final long nowMicros) {
final long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
/**
* Returns the earliest time that permits are available (with one caveat).
*
* @return the time that permits are available, or, if permits are available immediately, an
* arbitrary past or present time
*/
abstract long queryEarliestAvailable(long nowMicros);
/**
* Reserves the requested number of permits and returns the time that those permits can be used
* (with one caveat).
*
* @return the time that the permits may be used, or, if the permits may be used immediately, an
* arbitrary past or present time
*/
abstract long reserveEarliestAvailable(int permits, long nowMicros);
@Override
public String toString() {
return String.format(Locale.ROOT, "RateLimiter[stableRate=%3.1fqps]", getRate());
}
abstract static class SleepingStopwatch {
/** Constructor for use by subclasses. */
protected SleepingStopwatch() {}
protected abstract long readMicros();
protected abstract void sleepMicrosUninterruptibly(long micros);
public static SleepingStopwatch createFromSystemTimer() {
return new SleepingStopwatch() {
final long startTimeNano = System.nanoTime();
@Override
protected long readMicros() {
return MICROSECONDS.convert(System.nanoTime() - startTimeNano, NANOSECONDS);
}
@Override
protected void sleepMicrosUninterruptibly(final long micros) {
if (micros > 0) {
final long sleepMs = MICROSECONDS.toMillis(micros);
if (sleepMs > 0) {
try {
Thread.sleep(sleepMs);
} catch (final InterruptedException iex) {
Thread.currentThread().interrupt();
}
}
}
}
};
}
}
private static void checkPermits(final int permits) {
if(permits <= 0.0) {
throw new IllegalArgumentException(
"Requested permits (" + permits + ") must be positive");
}
}
}

View File

@ -0,0 +1,368 @@
/*
* Copyright (C) 2012 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.schabi.newpipe.downloader.ratelimiting.limiter;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.util.concurrent.TimeUnit;
abstract class SmoothRateLimiter extends RateLimiter {
/*
* How is the RateLimiter designed, and why?
*
* The primary feature of a RateLimiter is its "stable rate", the maximum rate that it should
* allow in normal conditions. This is enforced by "throttling" incoming requests as needed. For
* example, we could compute the appropriate throttle time for an incoming request, and make the
* calling thread wait for that time.
*
* The simplest way to maintain a rate of QPS is to keep the timestamp of the last granted
* request, and ensure that (1/QPS) seconds have elapsed since then. For example, for a rate of
* QPS=5 (5 tokens per second), if we ensure that a request isn't granted earlier than 200ms after
* the last one, then we achieve the intended rate. If a request comes and the last request was
* granted only 100ms ago, then we wait for another 100ms. At this rate, serving 15 fresh permits
* (i.e. for an acquire(15) request) naturally takes 3 seconds.
*
* It is important to realize that such a RateLimiter has a very superficial memory of the past:
* it only remembers the last request. What if the RateLimiter was unused for a long period of
* time, then a request arrived and was immediately granted? This RateLimiter would immediately
* forget about that past underutilization. This may result in either underutilization or
* overflow, depending on the real world consequences of not using the expected rate.
*
* Past underutilization could mean that excess resources are available. Then, the RateLimiter
* should speed up for a while, to take advantage of these resources. This is important when the
* rate is applied to networking (limiting bandwidth), where past underutilization typically
* translates to "almost empty buffers", which can be filled immediately.
*
* On the other hand, past underutilization could mean that "the server responsible for handling
* the request has become less ready for future requests", i.e. its caches become stale, and
* requests become more likely to trigger expensive operations (a more extreme case of this
* example is when a server has just booted, and it is mostly busy with getting itself up to
* speed).
*
* To deal with such scenarios, we add an extra dimension, that of "past underutilization",
* modeled by "storedPermits" variable. This variable is zero when there is no underutilization,
* and it can grow up to maxStoredPermits, for sufficiently large underutilization. So, the
* requested permits, by an invocation acquire(permits), are served from:
*
* - stored permits (if available)
*
* - fresh permits (for any remaining permits)
*
* How this works is best explained with an example:
*
* For a RateLimiter that produces 1 token per second, every second that goes by with the
* RateLimiter being unused, we increase storedPermits by 1. Say we leave the RateLimiter unused
* for 10 seconds (i.e., we expected a request at time X, but we are at time X + 10 seconds before
* a request actually arrives; this is also related to the point made in the last paragraph), thus
* storedPermits becomes 10.0 (assuming maxStoredPermits >= 10.0). At that point, a request of
* acquire(3) arrives. We serve this request out of storedPermits, and reduce that to 7.0 (how
* this is translated to throttling time is discussed later). Immediately after, assume that an
* acquire(10) request arriving. We serve the request partly from storedPermits, using all the
* remaining 7.0 permits, and the remaining 3.0, we serve them by fresh permits produced by the
* rate limiter.
*
* We already know how much time it takes to serve 3 fresh permits: if the rate is
* "1 token per second", then this will take 3 seconds. But what does it mean to serve 7 stored
* permits? As explained above, there is no unique answer. If we are primarily interested to deal
* with underutilization, then we want stored permits to be given out /faster/ than fresh ones,
* because underutilization = free resources for the taking. If we are primarily interested to
* deal with overflow, then stored permits could be given out /slower/ than fresh ones. Thus, we
* require a (different in each case) function that translates storedPermits to throttling time.
*
* This role is played by storedPermitsToWaitTime(double storedPermits, double permitsToTake). The
* underlying model is a continuous function mapping storedPermits (from 0.0 to maxStoredPermits)
* onto the 1/rate (i.e. intervals) that is effective at the given storedPermits. "storedPermits"
* essentially measure unused time; we spend unused time buying/storing permits. Rate is
* "permits / time", thus "1 / rate = time / permits". Thus, "1/rate" (time / permits) times
* "permits" gives time, i.e., integrals on this function (which is what storedPermitsToWaitTime()
* computes) correspond to minimum intervals between subsequent requests, for the specified number
* of requested permits.
*
* Here is an example of storedPermitsToWaitTime: If storedPermits == 10.0, and we want 3 permits,
* we take them from storedPermits, reducing them to 7.0, and compute the throttling for these as
* a call to storedPermitsToWaitTime(storedPermits = 10.0, permitsToTake = 3.0), which will
* evaluate the integral of the function from 7.0 to 10.0.
*
* Using integrals guarantees that the effect of a single acquire(3) is equivalent to {
* acquire(1); acquire(1); acquire(1); }, or { acquire(2); acquire(1); }, etc, since the integral
* of the function in [7.0, 10.0] is equivalent to the sum of the integrals of [7.0, 8.0], [8.0,
* 9.0], [9.0, 10.0] (and so on), no matter what the function is. This guarantees that we handle
* correctly requests of varying weight (permits), /no matter/ what the actual function is - so we
* can tweak the latter freely. (The only requirement, obviously, is that we can compute its
* integrals).
*
* Note well that if, for this function, we chose a horizontal line, at height of exactly (1/QPS),
* then the effect of the function is non-existent: we serve storedPermits at exactly the same
* cost as fresh ones (1/QPS is the cost for each). We use this trick later.
*
* If we pick a function that goes /below/ that horizontal line, it means that we reduce the area
* of the function, thus time. Thus, the RateLimiter becomes /faster/ after a period of
* underutilization. If, on the other hand, we pick a function that goes /above/ that horizontal
* line, then it means that the area (time) is increased, thus storedPermits are more costly than
* fresh permits, thus the RateLimiter becomes /slower/ after a period of underutilization.
*
* Last, but not least: consider a RateLimiter with rate of 1 permit per second, currently
* completely unused, and an expensive acquire(100) request comes. It would be nonsensical to just
* wait for 100 seconds, and /then/ start the actual task. Why wait without doing anything? A much
* better approach is to /allow/ the request right away (as if it was an acquire(1) request
* instead), and postpone /subsequent/ requests as needed. In this version, we allow starting the
* task immediately, and postpone by 100 seconds future requests, thus we allow for work to get
* done in the meantime instead of waiting idly.
*
* This has important consequences: it means that the RateLimiter doesn't remember the time of the
* _last_ request, but it remembers the (expected) time of the _next_ request. This also enables
* us to tell immediately (see tryAcquire(timeout)) whether a particular timeout is enough to get
* us to the point of the next scheduling time, since we always maintain that. And what we mean by
* "an unused RateLimiter" is also defined by that notion: when we observe that the
* "expected arrival time of the next request" is actually in the past, then the difference (now -
* past) is the amount of time that the RateLimiter was formally unused, and it is that amount of
* time which we translate to storedPermits. (We increase storedPermits with the amount of permits
* that would have been produced in that idle time). So, if rate == 1 permit per second, and
* arrivals come exactly one second after the previous, then storedPermits is _never_ increased --
* we would only increase it for arrivals _later_ than the expected one second.
*/
/**
* This implements the following function where coldInterval = coldFactor * stableInterval.
*
* <pre>
* ^ throttling
* |
* cold + /
* interval | /.
* | / .
* | / . "warmup period" is the area of the trapezoid between
* | / . thresholdPermits and maxPermits
* | / .
* | / .
* | / .
* stable +----------/ WARM .
* interval | . UP .
* | . PERIOD.
* | . .
* 0 +----------+-------+-------------- storedPermits
* 0 thresholdPermits maxPermits
* </pre>
*
* Before going into the details of this particular function, let's keep in mind the basics:
*
* <ol>
* <li>The state of the RateLimiter (storedPermits) is a vertical line in this figure.
* <li>When the RateLimiter is not used, this goes right (up to maxPermits)
* <li>When the RateLimiter is used, this goes left (down to zero), since if we have
* storedPermits, we serve from those first
* <li>When _unused_, we go right at a constant rate! The rate at which we move to the right is
* chosen as maxPermits / warmupPeriod. This ensures that the time it takes to go from 0 to
* maxPermits is equal to warmupPeriod.
* <li>When _used_, the time it takes, as explained in the introductory class note, is equal to
* the integral of our function, between X permits and X-K permits, assuming we want to
* spend K saved permits.
* </ol>
*
* <p>In summary, the time it takes to move to the left (spend K permits), is equal to the area of
* the function of width == K.
*
* <p>Assuming we have saturated demand, the time to go from maxPermits to thresholdPermits is
* equal to warmupPeriod. And the time to go from thresholdPermits to 0 is warmupPeriod/2. (The
* reason that this is warmupPeriod/2 is to maintain the behavior of the original implementation
* where coldFactor was hard coded as 3.)
*
* <p>It remains to calculate thresholdsPermits and maxPermits.
*
* <ul>
* <li>The time to go from thresholdPermits to 0 is equal to the integral of the function
* between 0 and thresholdPermits. This is thresholdPermits * stableIntervals. By (5) it is
* also equal to warmupPeriod/2. Therefore
* <blockquote>
* thresholdPermits = 0.5 * warmupPeriod / stableInterval
* </blockquote>
* <li>The time to go from maxPermits to thresholdPermits is equal to the integral of the
* function between thresholdPermits and maxPermits. This is the area of the pictured
* trapezoid, and it is equal to 0.5 * (stableInterval + coldInterval) * (maxPermits -
* thresholdPermits). It is also equal to warmupPeriod, so
* <blockquote>
* maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)
* </blockquote>
* </ul>
*/
static final class SmoothWarmingUp extends SmoothRateLimiter {
private final long warmupPeriodMicros;
/**
* The slope of the line from the stable interval (when permits == 0), to the cold interval
* (when permits == maxPermits)
*/
private double slope;
private double thresholdPermits;
private final double coldFactor;
SmoothWarmingUp(
final SleepingStopwatch stopwatch,
final long warmupPeriod,
final TimeUnit timeUnit,
final double coldFactor) {
super(stopwatch);
this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod);
this.coldFactor = coldFactor;
}
@Override
void doSetRate(final double permitsPerSecond, final double stableIntervalMicros) {
final double oldMaxPermits = maxPermits;
final double coldIntervalMicros = stableIntervalMicros * coldFactor;
thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
maxPermits =
thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);
slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = 0.0;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? maxPermits // initial state is cold
: storedPermits * maxPermits / oldMaxPermits;
}
}
@Override
long storedPermitsToWaitTime(final double storedPermits, double permitsToTake) {
final double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
long micros = 0;
// measuring the integral on the right part of the function (the climbing line)
if (availablePermitsAboveThreshold > 0.0) {
final double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
final double length =
permitsToTime(availablePermitsAboveThreshold)
+ permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
micros = (long) (permitsAboveThresholdToTake * length / 2.0);
permitsToTake -= permitsAboveThresholdToTake;
}
// measuring the integral on the left part of the function (the horizontal line)
micros += (long) (stableIntervalMicros * permitsToTake);
return micros;
}
private double permitsToTime(final double permits) {
return stableIntervalMicros + permits * slope;
}
@Override
double coolDownIntervalMicros() {
return warmupPeriodMicros / maxPermits;
}
}
/** The currently stored permits. */
double storedPermits;
/** The maximum number of stored permits. */
double maxPermits;
/**
* The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
* per second has a stable interval of 200ms.
*/
double stableIntervalMicros;
/**
* The time when the next request (no matter its size) will be granted. After granting a request,
* this is pushed further in the future. Large requests push this further than small requests.
*/
private long nextFreeTicketMicros = 0L; // could be either in the past or future
private SmoothRateLimiter(final SleepingStopwatch stopwatch) {
super(stopwatch);
}
@Override
final void doSetRate(final double permitsPerSecond, final long nowMicros) {
resync(nowMicros);
final double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros);
@Override
final double doGetRate() {
return SECONDS.toMicros(1L) / stableIntervalMicros;
}
@Override
final long queryEarliestAvailable(final long nowMicros) {
return nextFreeTicketMicros;
}
@Override
final long reserveEarliestAvailable(final int requiredPermits, final long nowMicros) {
resync(nowMicros);
final long returnValue = nextFreeTicketMicros;
final double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
final double freshPermits = requiredPermits - storedPermitsToSpend;
final long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
this.nextFreeTicketMicros = saturatedAdd(nextFreeTicketMicros, waitMicros);
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
/**
* Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case
* {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively.
*
* @since 20.0
*/
@SuppressWarnings("ShortCircuitBoolean")
static long saturatedAdd(final long a, final long b) {
final long naiveSum = a + b;
if ((a ^ b) < 0 || (a ^ naiveSum) >= 0) {
// If a and b have different signs or a has the same sign as the result then there was no
// overflow, return.
return naiveSum;
}
// we did over/under flow, if the sign is negative we should return MAX otherwise MIN
return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1);
}
/**
* Translates a specified portion of our currently stored permits which we want to spend/acquire,
* into a throttling time. Conceptually, this evaluates the integral of the underlying function we
* use, for the range of [(storedPermits - permitsToTake), storedPermits].
*
* <p>This always holds: {@code 0 <= permitsToTake <= storedPermits}
*/
abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake);
/**
* Returns the number of microseconds during cool down that we have to wait to get a new permit.
*/
abstract double coolDownIntervalMicros();
/** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
void resync(final long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
final double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
}

View File

@ -0,0 +1,10 @@
/**
* Contains classes for Rate limiting.
* <br/>
* This code is based on
* <a href="https://github.com/google/guava/">google/guava</a>
* (Apache 2.0 license) but was modified/refactored for our use.
*
* @author litetex
*/
package org.schabi.newpipe.downloader.ratelimiting.limiter;

View File

@ -133,7 +133,7 @@ public class PeertubeCommentsExtractorTest {
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
extractor = (PeertubeCommentsExtractor) PeerTube
.getCommentsExtractor("https://share.tube/w/vxu4uTstUBAUromWwXGHrq");
.getCommentsExtractor("https://framatube.org/w/kkGMgK9ZtnKfYAgnEtQxbv");
comments = extractor.getInitialPage();
}
@ -141,16 +141,16 @@ public class PeertubeCommentsExtractorTest {
void testGetComments() throws IOException, ExtractionException {
assertFalse(comments.getItems().isEmpty());
final Optional<CommentsInfoItem> nestedCommentHeadOpt =
findCommentWithId("9770", comments.getItems());
findCommentWithId("34293", comments.getItems());
assertTrue(nestedCommentHeadOpt.isPresent());
assertTrue(findNestedCommentWithId("9773", nestedCommentHeadOpt.get()), "The nested comment replies were not found");
assertTrue(findNestedCommentWithId("34294", nestedCommentHeadOpt.get()), "The nested " +
"comment replies were not found");
}
@Test
void testHasCreatorReply() {
assertCreatorReply("9770", true);
assertCreatorReply("9852", false);
assertCreatorReply("11239", false);
// NOTE: There is currently no creator replied comment
assertCreatorReply("34293", false);
}
private static void assertCreatorReply(final String id, final boolean expected) {

View File

@ -1,5 +1,8 @@
package org.schabi.newpipe.extractor.services.peertube;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.schabi.newpipe.extractor.ServiceList.PeerTube;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -19,9 +22,6 @@ import java.util.Locale;
import javax.annotation.Nullable;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.schabi.newpipe.extractor.ServiceList.PeerTube;
public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractorTest {
private static final String BASE_URL = "/videos/watch/";
@ -149,7 +149,7 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());;
NewPipe.init(DownloaderTestImpl.getInstance());
// setting instance might break test when running in parallel (!)
PeerTube.setInstance(new PeertubeInstance(INSTANCE));
extractor = PeerTube.getStreamExtractor(URL);
@ -238,21 +238,21 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractor
@Test
public void testGetEmptyDescription() throws Exception {
StreamExtractor extractorEmpty = PeerTube.getStreamExtractor("https://framatube.org/api/v1/videos/d5907aad-2252-4207-89ec-a4b687b9337d");
final StreamExtractor extractorEmpty = PeerTube.getStreamExtractor("https://framatube.org/api/v1/videos/d5907aad-2252-4207-89ec-a4b687b9337d");
extractorEmpty.fetchPage();
assertEquals("", extractorEmpty.getDescription().getContent());
}
@Test
public void testGetSmallDescription() throws Exception {
StreamExtractor extractorSmall = PeerTube.getStreamExtractor("https://peertube.cpy.re/videos/watch/d2a5ec78-5f85-4090-8ec5-dc1102e022ea");
final StreamExtractor extractorSmall = PeerTube.getStreamExtractor("https://peertube.cpy.re/videos/watch/d2a5ec78-5f85-4090-8ec5-dc1102e022ea");
extractorSmall.fetchPage();
assertEquals("https://www.kickstarter.com/projects/1587081065/nothing-to-hide-the-documentary", extractorSmall.getDescription().getContent());
}
@Test
public void testGetSupportInformation() throws ExtractionException, IOException {
StreamExtractor supportInfoExtractor = PeerTube.getStreamExtractor("https://framatube.org/videos/watch/ee408ec8-07cd-4e35-b884-fb681a4b9d37");
final StreamExtractor supportInfoExtractor = PeerTube.getStreamExtractor("https://framatube.org/videos/watch/ee408ec8-07cd-4e35-b884-fb681a4b9d37");
supportInfoExtractor.fetchPage();
assertEquals("https://utip.io/chatsceptique", supportInfoExtractor.getSupportInfo());
}

View File

@ -1,7 +1,6 @@
package org.schabi.newpipe.extractor.services.peertube.search;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.InfoItem;
@ -70,8 +69,7 @@ public class PeertubeSearchExtractorTest {
public static class PagingTest {
@Test
@Disabled("Exception in CI: javax.net.ssl.SSLHandshakeException: PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed")
public void duplicatedItemsCheck() throws Exception {
void duplicatedItemsCheck() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
final SearchExtractor extractor = PeerTube.getSearchExtractor("internet", singletonList(VIDEOS), "");
extractor.fetchPage();

View File

@ -16,7 +16,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.ExtractorAsserts;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
@ -25,7 +24,6 @@ import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
import org.schabi.newpipe.extractor.services.BaseChannelExtractorTest;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeChannelExtractor;
@ -43,13 +41,13 @@ public class YoutubeChannelExtractorTest {
public static class NotAvailable {
@BeforeAll
public static void setUp() throws IOException {
public static void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "notAvailable"));
}
@Test
public void deletedFetch() throws Exception {
void deletedFetch() throws Exception {
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCAUc4iz6edWerIjlnL8OSSw");
@ -57,7 +55,7 @@ public class YoutubeChannelExtractorTest {
}
@Test
public void nonExistentFetch() throws Exception {
void nonExistentFetch() throws Exception {
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/DOESNT-EXIST");
@ -65,72 +63,72 @@ public class YoutubeChannelExtractorTest {
}
@Test
public void accountTerminatedTOSFetch() throws Exception {
void accountTerminatedTOSFetch() throws Exception {
// "This account has been terminated for a violation of YouTube's Terms of Service."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCTGjY2I-ZUGnwVoWAGRd7XQ");
AccountTerminatedException ex =
final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}
@Test
public void accountTerminatedCommunityFetch() throws Exception {
void accountTerminatedCommunityFetch() throws Exception {
// "This account has been terminated for violating YouTube's Community Guidelines."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UC0AuOxCr9TZ0TtEgL1zpIgA");
AccountTerminatedException ex =
final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}
@Test
public void accountTerminatedHateFetch() throws Exception {
void accountTerminatedHateFetch() throws Exception {
// "This account has been terminated due to multiple or severe violations
// of YouTube's policy prohibiting hate speech."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCPWXIOPK-9myzek6jHR5yrg");
AccountTerminatedException ex =
final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}
@Test
public void accountTerminatedBullyFetch() throws Exception {
void accountTerminatedBullyFetch() throws Exception {
// "This account has been terminated due to multiple or severe violations
// of YouTube's policy prohibiting content designed to harass, bully or threaten."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://youtube.com/channel/UCB1o7_gbFp2PLsamWxFenBg");
AccountTerminatedException ex =
final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}
@Test
public void accountTerminatedSpamFetch() throws Exception {
void accountTerminatedSpamFetch() throws Exception {
// "This account has been terminated due to multiple or severe violations
// of YouTube's policy against spam, deceptive practices and misleading content
// or other Terms of Service violations."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCoaO4U_p7G7AwalqSbGCZOA");
AccountTerminatedException ex =
final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}
@Test
public void accountTerminatedCopyrightFetch() throws Exception {
void accountTerminatedCopyrightFetch() throws Exception {
// "This account has been terminated because we received multiple third-party claims
// of copyright infringement regarding material that the user posted."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCI4i4RgFT5ilfMpna4Z_Y8w");
AccountTerminatedException ex =
final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}
@ -139,7 +137,7 @@ public class YoutubeChannelExtractorTest {
static class SystemTopic {
@BeforeAll
static void setUp() throws IOException {
static void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "systemTopic"));
}
@ -169,26 +167,31 @@ public class YoutubeChannelExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertEquals("Gronkh", extractor.getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("UCYJ61XIK64sp6ZFFS8sctxw", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCYJ61XIK64sp6ZFFS8sctxw", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("http://www.youtube.com/@Gronkh", extractor.getOriginalUrl());
@ -198,26 +201,31 @@ public class YoutubeChannelExtractorTest {
// ChannelExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testDescription() throws Exception {
assertContains("Ungebremster Spieltrieb seit 1896.", extractor.getDescription());
}
@Override
@Test
public void testAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getAvatars());
}
@Override
@Test
public void testBanners() throws Exception {
YoutubeTestsUtils.testImages(extractor.getBanners());
}
@Override
@Test
public void testFeedUrl() throws Exception {
assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=UCYJ61XIK64sp6ZFFS8sctxw", extractor.getFeedUrl());
}
@Override
@Test
public void testSubscriberCount() throws Exception {
ExtractorAsserts.assertGreaterOrEqual(4_900_000, extractor.getSubscriberCount());
@ -262,26 +270,31 @@ public class YoutubeChannelExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertEquals("Vsauce", extractor.getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("UC6nSFpj9HTCZ5t-N3Rm3-HA", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UC6nSFpj9HTCZ5t-N3Rm3-HA", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/user/Vsauce", extractor.getOriginalUrl());
@ -291,31 +304,37 @@ public class YoutubeChannelExtractorTest {
// ChannelExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testDescription() throws Exception {
assertContains("Our World is Amazing", extractor.getDescription());
}
@Override
@Test
public void testAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getAvatars());
}
@Override
@Test
public void testBanners() throws Exception {
YoutubeTestsUtils.testImages(extractor.getBanners());
}
@Override
@Test
public void testFeedUrl() throws Exception {
assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=UC6nSFpj9HTCZ5t-N3Rm3-HA", extractor.getFeedUrl());
}
@Override
@Test
public void testSubscriberCount() throws Exception {
ExtractorAsserts.assertGreaterOrEqual(17_000_000, extractor.getSubscriberCount());
}
@Override
@Test
public void testVerified() throws Exception {
assertTrue(extractor.isVerified());
@ -355,26 +374,31 @@ public class YoutubeChannelExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertTrue(extractor.getName().startsWith("Kurzgesagt"));
}
@Override
@Test
public void testId() throws Exception {
assertEquals("UCsXVk37bltHxD1rDPwtNM8Q", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCsXVk37bltHxD1rDPwtNM8Q", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCsXVk37bltHxD1rDPwtNM8Q", extractor.getOriginalUrl());
@ -384,6 +408,7 @@ public class YoutubeChannelExtractorTest {
// ChannelExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testDescription() throws Exception {
ExtractorAsserts.assertContains("science", extractor.getDescription());
@ -392,26 +417,31 @@ public class YoutubeChannelExtractorTest {
//assertTrue(description, description.contains("Currently we make one animation video per month"));
}
@Override
@Test
public void testAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getAvatars());
}
@Override
@Test
public void testBanners() throws Exception {
YoutubeTestsUtils.testImages(extractor.getBanners());
}
@Override
@Test
public void testFeedUrl() throws Exception {
assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=UCsXVk37bltHxD1rDPwtNM8Q", extractor.getFeedUrl());
}
@Override
@Test
public void testSubscriberCount() throws Exception {
ExtractorAsserts.assertGreaterOrEqual(17_000_000, extractor.getSubscriberCount());
}
@Override
@Test
public void testVerified() throws Exception {
assertTrue(extractor.isVerified());
@ -442,8 +472,8 @@ public class YoutubeChannelExtractorTest {
@BeforeAll
public static void setUp() throws Exception {
// Test is not deterministic, mocks can't be used
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "kurzgesagtAdditional1"));
extractor = (YoutubeChannelExtractor) YouTube.getChannelExtractor(
"https://www.youtube.com/channel/UCsXVk37bltHxD1rDPwtNM8Q");
extractor.fetchPage();
@ -453,7 +483,10 @@ public class YoutubeChannelExtractorTest {
}
@Test
public void testGetPageInNewExtractor() throws Exception {
void testGetPageInNewExtractor() throws Exception {
// Init downloader again for mock as otherwise request confusion occurs when using Mock
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "kurzgesagtAdditional2"));
final ChannelExtractor newExtractor = YouTube.getChannelExtractor(extractor.getUrl());
newExtractor.fetchPage();
final ChannelTabExtractor newTabExtractor = YouTube.getChannelTabExtractor(
@ -478,26 +511,31 @@ public class YoutubeChannelExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertEquals("Captain Disillusion", extractor.getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("UCEOXxzW2vU0P-0THehuIIeg", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCEOXxzW2vU0P-0THehuIIeg", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/user/CaptainDisillusion/videos", extractor.getOriginalUrl());
@ -507,31 +545,37 @@ public class YoutubeChannelExtractorTest {
// ChannelExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testDescription() throws Exception {
assertContains("In a world where", extractor.getDescription());
}
@Override
@Test
public void testAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getAvatars());
}
@Override
@Test
public void testBanners() throws Exception {
YoutubeTestsUtils.testImages(extractor.getBanners());
}
@Override
@Test
public void testFeedUrl() throws Exception {
assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=UCEOXxzW2vU0P-0THehuIIeg", extractor.getFeedUrl());
}
@Override
@Test
public void testSubscriberCount() throws Exception {
ExtractorAsserts.assertGreaterOrEqual(2_000_000, extractor.getSubscriberCount());
}
@Override
@Test
public void testVerified() throws Exception {
assertTrue(extractor.isVerified());
@ -571,26 +615,31 @@ public class YoutubeChannelExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertEquals("random channel", extractor.getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("UCUaQMQS9lY5lit3vurpXQ6w", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCUaQMQS9lY5lit3vurpXQ6w", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCUaQMQS9lY5lit3vurpXQ6w", extractor.getOriginalUrl());
@ -600,31 +649,37 @@ public class YoutubeChannelExtractorTest {
// ChannelExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testDescription() throws Exception {
assertContains("Hey there iu will upoload a load of pranks onto this channel", extractor.getDescription());
}
@Override
@Test
public void testAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getAvatars());
}
@Override
@Test
public void testBanners() throws Exception {
YoutubeTestsUtils.testImages(extractor.getBanners());
}
@Override
@Test
public void testFeedUrl() throws Exception {
assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=UCUaQMQS9lY5lit3vurpXQ6w", extractor.getFeedUrl());
}
@Override
@Test
public void testSubscriberCount() throws Exception {
ExtractorAsserts.assertGreaterOrEqual(50, extractor.getSubscriberCount());
}
@Override
@Test
public void testVerified() throws Exception {
assertFalse(extractor.isVerified());
@ -662,26 +717,31 @@ public class YoutubeChannelExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertEquals("Sports", extractor.getName());
}
@Override
@Test
public void testId() throws Exception {
assertEquals("UCEgdi0XIXXZ-qJOFPf4JSKw", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCEgdi0XIXXZ-qJOFPf4JSKw", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/channel/UCEgdi0XIXXZ-qJOFPf4JSKw", extractor.getOriginalUrl());
@ -697,27 +757,32 @@ public class YoutubeChannelExtractorTest {
assertNull(extractor.getDescription());
}
@Override
@Test
public void testAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getAvatars());
}
@Override
@Test
public void testBanners() {
// A CarouselHeaderRenderer doesn't contain a banner
assertEmpty(extractor.getBanners());
}
@Override
@Test
public void testFeedUrl() throws Exception {
assertEquals("https://www.youtube.com/feeds/videos.xml?channel_id=UCEgdi0XIXXZ-qJOFPf4JSKw", extractor.getFeedUrl());
}
@Override
@Test
public void testSubscriberCount() throws Exception {
ExtractorAsserts.assertGreaterOrEqual(70_000_000, extractor.getSubscriberCount());
}
@Override
@Test
public void testVerified() throws Exception {
assertTrue(extractor.isVerified());

View File

@ -1,31 +1,5 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.annotation.Nonnull;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -50,6 +24,33 @@ import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Stream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.StringReader;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.annotation.Nonnull;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
/**
* Test for YouTube DASH manifest creators.
*
@ -80,19 +81,22 @@ import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
* </p>
*/
class YoutubeDashManifestCreatorsTest {
private static final String RESOURCE_PATH =
DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/dashManifest";
// Setting a higher number may let Google video servers return 403s
private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 3;
private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM";
private static final String URL = "https://www.youtube.com/watch?v=DJ8GQUNUXGM";
private static YoutubeStreamExtractor extractor;
private static long videoLength;
@BeforeAll
public static void setUp() throws Exception {
YoutubeParsingHelper.resetClientVersion();
YoutubeParsingHelper.setNumberGenerator(new Random(1));
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderTestImpl.getInstance());
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH));
extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url);
extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(URL);
extractor.fetchPage();
videoLength = extractor.getLength();
}

View File

@ -81,7 +81,8 @@ public class YoutubeFeedExtractorTest {
public static class NotAvailable {
@BeforeAll
public static void setUp() throws IOException {
public static void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "notAvailable/"));
}

View File

@ -4,40 +4,48 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.ExtractorAsserts;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.io.IOException;
public class YoutubeJavaScriptExtractorTest {
class YoutubeJavaScriptExtractorTest {
private static final String RESOURCE_PATH =
DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/jsExtractor/";
@BeforeEach
public void setup() throws IOException {
NewPipe.init(DownloaderTestImpl.getInstance());
public void setup() {
YoutubeTestsUtils.ensureStateless();
}
@Test
public void testExtractJavaScriptUrlIframe() throws ParsingException {
void testExtractJavaScriptUrlIframe() throws ParsingException {
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "urlWithIframeResource"));
assertTrue(YoutubeJavaScriptExtractor.extractJavaScriptUrlWithIframeResource()
.endsWith("base.js"));
}
@Test
public void testExtractJavaScriptUrlEmbed() throws ParsingException {
void testExtractJavaScriptUrlEmbed() throws ParsingException {
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "embedWatchPage"));
assertTrue(YoutubeJavaScriptExtractor.extractJavaScriptUrlWithEmbedWatchPage("d4IGg5dqeO8")
.endsWith("base.js"));
}
@Test
public void testExtractJavaScript__success() throws ParsingException {
String playerJsCode = YoutubeJavaScriptExtractor.extractJavaScriptPlayerCode("d4IGg5dqeO8");
void testExtractJavaScript__success() throws ParsingException {
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "playerCode"));
final String playerJsCode = YoutubeJavaScriptExtractor.extractJavaScriptPlayerCode("d4IGg5dqeO8");
assertPlayerJsCode(playerJsCode);
}
@Test
public void testExtractJavaScript__invalidVideoId__success() throws ParsingException {
void testExtractJavaScript__invalidVideoId__success() throws ParsingException {
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "playerCodeInvalidVideoId"));
String playerJsCode = YoutubeJavaScriptExtractor.extractJavaScriptPlayerCode("not_a_video_id");
assertPlayerJsCode(playerJsCode);

View File

@ -310,7 +310,7 @@ public class YoutubeMixPlaylistExtractorTest {
private static final String VIDEO_ID = "QMVCAPd5cwBcg";
@BeforeAll
public static void setUp() throws IOException {
public static void setUp() {
YoutubeTestsUtils.ensureStateless();
YoutubeParsingHelper.setConsentAccepted(true);
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "invalid"));
@ -336,72 +336,6 @@ public class YoutubeMixPlaylistExtractorTest {
}
}
public static class ChannelMix {
private static final String CHANNEL_ID = "UCXuqSBlHAE6Xw-yeJA0Tunw";
private static final String VIDEO_ID_OF_CHANNEL = "mnk6gnOBYIo";
private static final String CHANNEL_TITLE = "Linus Tech Tips";
@BeforeAll
public static void setUp() throws Exception {
YoutubeTestsUtils.ensureStateless();
YoutubeParsingHelper.setConsentAccepted(true);
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "channelMix"));
extractor = (YoutubeMixPlaylistExtractor) YouTube
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
+ "&list=RDCM" + CHANNEL_ID);
extractor.fetchPage();
}
@Test
void getName() throws Exception {
final String name = extractor.getName();
ExtractorAsserts.assertContains("Mix", name);
ExtractorAsserts.assertContains(CHANNEL_TITLE, name);
}
@Test
void getThumbnails() throws Exception {
YoutubeTestsUtils.testImages(extractor.getThumbnails());
extractor.getThumbnails().forEach(thumbnail ->
ExtractorAsserts.assertContains(VIDEO_ID_OF_CHANNEL, thumbnail.getUrl()));
}
@Test
void getInitialPage() throws Exception {
final InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
assertFalse(streams.getItems().isEmpty());
assertTrue(streams.hasNextPage());
}
@Test
void getPage() throws Exception {
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
NewPipe.getPreferredLocalization(), NewPipe.getPreferredContentCountry())
.value("videoId", VIDEO_ID_OF_CHANNEL)
.value("playlistId", "RDCM" + CHANNEL_ID)
.value("params", "OAE%3D")
.done())
.getBytes(StandardCharsets.UTF_8);
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(new Page(
YOUTUBEI_V1_URL + "next?" + DISABLE_PRETTY_PRINT_PARAMETER,
null, null, dummyCookie, body));
assertFalse(streams.getItems().isEmpty());
assertTrue(streams.hasNextPage());
}
@Test
void getStreamCount() {
assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount());
}
@Test
void getPlaylistType() throws ParsingException {
assertEquals(PlaylistInfo.PlaylistType.MIX_CHANNEL, extractor.getPlaylistType());
}
}
public static class GenreMix {
private static final String VIDEO_ID = "kINJeTNFbpg";
private static final String MIX_TITLE = "Mix Electronic music";

View File

@ -19,7 +19,7 @@ public class YoutubeParsingHelperTest {
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/";
@BeforeAll
public static void setUp() throws IOException {
public static void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "youtubeParsingHelper"));
}

View File

@ -40,7 +40,7 @@ public class YoutubePlaylistExtractorTest {
public static class NotAvailable {
@BeforeAll
public static void setUp() throws IOException {
public static void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "notAvailable"));
}
@ -76,26 +76,31 @@ public class YoutubePlaylistExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertTrue(extractor.getName().startsWith("Pop Music Playlist"));
}
@Override
@Test
public void testId() throws Exception {
assertEquals("PLMC9KNkIncKtPzgY-5rmhvj7fax8fdxoj", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/playlist?list=PLMC9KNkIncKtPzgY-5rmhvj7fax8fdxoj", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("http://www.youtube.com/watch?v=lp-EO5I60KA&list=PLMC9KNkIncKtPzgY-5rmhvj7fax8fdxoj", extractor.getOriginalUrl());
@ -105,11 +110,13 @@ public class YoutubePlaylistExtractorTest {
// ListExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor);
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
defaultTestMoreItems(extractor);
@ -119,11 +126,13 @@ public class YoutubePlaylistExtractorTest {
// PlaylistExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testThumbnails() throws Exception {
YoutubeTestsUtils.testImages(extractor.getThumbnails());
}
@Override
@Test
public void testBanners() throws ParsingException {
YoutubeTestsUtils.testImages(extractor.getBanners());
@ -134,17 +143,20 @@ public class YoutubePlaylistExtractorTest {
assertEquals("https://www.youtube.com/channel/UCs72iRpTEuwV3y6pdWYLgiw", extractor.getUploaderUrl());
}
@Override
@Test
public void testUploaderName() throws Exception {
final String uploaderName = extractor.getUploaderName();
ExtractorAsserts.assertContains("Just Hits", uploaderName);
}
@Override
@Test
public void testUploaderAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getUploaderAvatars());
}
@Override
@Test
public void testStreamCount() throws Exception {
ExtractorAsserts.assertGreater(100, extractor.getStreamCount());
@ -194,27 +206,32 @@ public class YoutubePlaylistExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
final String name = extractor.getName();
assertEquals("I Wanna Rock Super Gigantic Playlist 1: Hardrock, AOR, Metal and more !!! 5000 music videos !!!", name);
}
@Override
@Test
public void testId() throws Exception {
assertEquals("PLWwAypAcFRgKAIIFqBr9oy-ZYZnixa_Fj", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/playlist?list=PLWwAypAcFRgKAIIFqBr9oy-ZYZnixa_Fj", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/watch?v=8SbUC-UaAxE&list=PLWwAypAcFRgKAIIFqBr9oy-ZYZnixa_Fj", extractor.getOriginalUrl());
@ -224,11 +241,13 @@ public class YoutubePlaylistExtractorTest {
// ListExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor);
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
ListExtractor.InfoItemsPage<StreamInfoItem> currentPage = defaultTestMoreItems(extractor);
@ -244,11 +263,13 @@ public class YoutubePlaylistExtractorTest {
// PlaylistExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testThumbnails() throws Exception {
YoutubeTestsUtils.testImages(extractor.getThumbnails());
}
@Override
@Test
public void testBanners() throws ParsingException {
YoutubeTestsUtils.testImages(extractor.getBanners());
@ -259,16 +280,19 @@ public class YoutubePlaylistExtractorTest {
assertEquals("https://www.youtube.com/channel/UCHSPWoY1J5fbDVbcnyeqwdw", extractor.getUploaderUrl());
}
@Override
@Test
public void testUploaderName() throws Exception {
assertEquals("Tomas Nilsson TOMPA571", extractor.getUploaderName());
}
@Override
@Test
public void testUploaderAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getUploaderAvatars());
}
@Override
@Test
public void testStreamCount() throws Exception {
ExtractorAsserts.assertGreater(100, extractor.getStreamCount());
@ -308,26 +332,31 @@ public class YoutubePlaylistExtractorTest {
// Extractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testServiceId() {
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
}
@Override
@Test
public void testName() throws Exception {
assertTrue(extractor.getName().startsWith("Anatomy & Physiology"));
}
@Override
@Test
public void testId() throws Exception {
assertEquals("PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getId());
}
@Override
@Test
public void testUrl() throws ParsingException {
assertEquals("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getUrl());
}
@Override
@Test
public void testOriginalUrl() throws ParsingException {
assertEquals("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getOriginalUrl());
@ -337,11 +366,13 @@ public class YoutubePlaylistExtractorTest {
// ListExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testRelatedItems() throws Exception {
defaultTestRelatedItems(extractor);
}
@Override
@Test
public void testMoreRelatedItems() throws Exception {
assertFalse(extractor.getInitialPage().hasNextPage());
@ -351,11 +382,13 @@ public class YoutubePlaylistExtractorTest {
// PlaylistExtractor
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testThumbnails() throws Exception {
YoutubeTestsUtils.testImages(extractor.getThumbnails());
}
@Override
@Test
public void testBanners() throws ParsingException {
YoutubeTestsUtils.testImages(extractor.getBanners());
@ -366,17 +399,20 @@ public class YoutubePlaylistExtractorTest {
assertEquals("https://www.youtube.com/channel/UCX6b17PVsYBQ0ip5gyeme-Q", extractor.getUploaderUrl());
}
@Override
@Test
public void testUploaderName() throws Exception {
final String uploaderName = extractor.getUploaderName();
ExtractorAsserts.assertContains("CrashCourse", uploaderName);
}
@Override
@Test
public void testUploaderAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getUploaderAvatars());
}
@Override
@Test
public void testStreamCount() throws Exception {
ExtractorAsserts.assertGreater(40, extractor.getStreamCount());
@ -445,6 +481,7 @@ public class YoutubePlaylistExtractorTest {
extractor.getOriginalUrl());
}
@Disabled("Known problem, see https://github.com/TeamNewPipe/NewPipeExtractor/issues/1273")
@Test
@Override
public void testRelatedItems() throws Exception {
@ -479,6 +516,7 @@ public class YoutubePlaylistExtractorTest {
assertEquals("YouTube", extractor.getUploaderName());
}
@Override
@Test
public void testUploaderAvatars() throws Exception {
YoutubeTestsUtils.testImages(extractor.getUploaderAvatars());
@ -511,7 +549,7 @@ public class YoutubePlaylistExtractorTest {
public static class ContinuationsTests {
@BeforeAll
public static void setUp() throws IOException {
public static void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "continuations"));
}

View File

@ -1,24 +1,27 @@
package org.schabi.newpipe.extractor.services.youtube;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.NewPipe;
import javax.annotation.Nonnull;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import javax.annotation.Nonnull;
class YoutubeSignaturesTest {
private static final String RESOURCE_PATH =
DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/signatures";
@BeforeEach
void setUp() throws IOException {
NewPipe.init(DownloaderTestImpl.getInstance());
void setUp() {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH));
}
@ValueSource(strings = {

View File

@ -6,18 +6,20 @@ import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.io.IOException;
class YoutubeThrottlingParameterDeobfuscationTest {
private static final String RESOURCE_PATH =
DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/parameterDeobf";
@BeforeEach
void setup() throws IOException {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH));
}
@Test

View File

@ -28,14 +28,13 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
/**
* Test for {@link KioskInfo}
*/
public class YoutubeTrendingKioskInfoTest {
class YoutubeTrendingKioskInfoTest {
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "kiosk";
@ -46,24 +45,24 @@ public class YoutubeTrendingKioskInfoTest {
throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH));
LinkHandlerFactory LinkHandlerFactory = ((StreamingService) YouTube).getKioskList().getListLinkHandlerFactoryByType("Trending");
LinkHandlerFactory linkHandlerFactory = YouTube.getKioskList().getListLinkHandlerFactoryByType("Trending");
kioskInfo = KioskInfo.getInfo(YouTube, LinkHandlerFactory.fromId("Trending").getUrl());
kioskInfo = KioskInfo.getInfo(YouTube, linkHandlerFactory.fromId("Trending").getUrl());
}
@Test
public void getStreams() {
void getStreams() {
assertFalse(kioskInfo.getRelatedItems().isEmpty());
}
@Test
public void getId() {
void getId() {
assertTrue(kioskInfo.getId().equals("Trending")
|| kioskInfo.getId().equals("Trends"));
}
@Test
public void getName() {
void getName() {
assertFalse(kioskInfo.getName().isEmpty());
}
}

View File

@ -4,28 +4,34 @@ import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static java.util.Collections.singletonList;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.services.DefaultSearchExtractorTest;
import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.annotation.Nullable;
// Doesn't work with mocks. Makes request with different `dataToSend` I think
public class YoutubeMusicSearchExtractorTest {
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/musicSearch/";
private static final String BASE_SEARCH_URL = "music.youtube.com/search?q=";
public static class MusicSongs extends DefaultSearchExtractorTest {
private static SearchExtractor extractor;
private static final String QUERY = "mocromaniac";
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicSongs"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), "");
extractor.fetchPage();
}
@ -34,8 +40,8 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@ -47,7 +53,9 @@ public class YoutubeMusicSearchExtractorTest {
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicVideos"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_VIDEOS), "");
extractor.fetchPage();
}
@ -56,8 +64,8 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@ -69,7 +77,9 @@ public class YoutubeMusicSearchExtractorTest {
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicAlbums"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_ALBUMS), "");
extractor.fetchPage();
}
@ -78,8 +88,8 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + URLEncoder.encode(QUERY, StandardCharsets.UTF_8); }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + URLEncoder.encode(QUERY, StandardCharsets.UTF_8); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.PLAYLIST; }
@ -91,7 +101,9 @@ public class YoutubeMusicSearchExtractorTest {
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicPlaylists"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_PLAYLISTS), "");
extractor.fetchPage();
}
@ -100,21 +112,22 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.PLAYLIST; }
}
@Disabled
public static class MusicArtists extends DefaultSearchExtractorTest {
private static SearchExtractor extractor;
private static final String QUERY = "kevin";
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicArtists"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_ARTISTS), "");
extractor.fetchPage();
}
@ -123,22 +136,22 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + QUERY; }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + QUERY; }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.CHANNEL; }
}
@Disabled("Currently constantly switching between \"Did you mean\" and \"Showing results for ...\" occurs")
public static class Suggestion extends DefaultSearchExtractorTest {
private static SearchExtractor extractor;
private static final String QUERY = "megaman x3";
private static final boolean CORRECTED = true;
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "suggestion"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), "");
extractor.fetchPage();
}
@ -147,12 +160,11 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + URLEncoder.encode(QUERY, StandardCharsets.UTF_8); }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + URLEncoder.encode(QUERY, StandardCharsets.UTF_8); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return "mega man x3"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@Override public boolean isCorrectedSearch() { return CORRECTED; }
}
public static class CorrectedSearch extends DefaultSearchExtractorTest {
@ -162,7 +174,9 @@ public class YoutubeMusicSearchExtractorTest {
@BeforeAll
public static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "corrected"));
extractor = YouTube.getSearchExtractor(QUERY, singletonList(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), "");
extractor.fetchPage();
}
@ -171,8 +185,8 @@ public class YoutubeMusicSearchExtractorTest {
@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
@Override public String expectedOriginalUrlContains() { return "music.youtube.com/search?q=" + URLEncoder.encode(QUERY); }
@Override public String expectedUrlContains() { return BASE_SEARCH_URL + URLEncoder.encode(QUERY, StandardCharsets.UTF_8); }
@Override public String expectedOriginalUrlContains() { return BASE_SEARCH_URL + URLEncoder.encode(QUERY, StandardCharsets.UTF_8); }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return EXPECTED_SUGGESTION; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }

View File

@ -13,6 +13,7 @@ import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeS
import static java.util.Collections.singletonList;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.InfoItem;
@ -219,6 +220,7 @@ public class YoutubeSearchExtractorTest {
// Test Overrides
//////////////////////////////////////////////////////////////////////////*/
@Override
@Test
public void testMoreRelatedItems() throws Exception {
final ListExtractor.InfoItemsPage<InfoItem> initialPage = extractor().getInitialPage();
@ -247,6 +249,7 @@ public class YoutubeSearchExtractorTest {
}
}
@Disabled("Known problem, see https://github.com/TeamNewPipe/NewPipeExtractor/issues/1274")
public static class MetaInfoTest extends DefaultSearchExtractorTest {
private static SearchExtractor extractor;
private static final String QUERY = "Covid";
@ -308,7 +311,7 @@ public class YoutubeSearchExtractorTest {
void testAtLeastOneVerified() throws IOException, ExtractionException {
final List<InfoItem> items = extractor.getInitialPage().getItems();
boolean verified = false;
for (InfoItem item : items) {
for (final InfoItem item : items) {
if (((ChannelInfoItem) item).isVerified()) {
verified = true;
break;
@ -380,6 +383,12 @@ public class YoutubeSearchExtractorTest {
final List<InfoItem> items = extractor.getInitialPage().getItems();
assertNotNull(((StreamInfoItem) items.get(0)).getShortDescription());
}
@Disabled("Irrelevant - sometimes suggestions show up, sometimes not")
@Override
public void testSearchSuggestion() throws Exception {
super.testSearchSuggestion();
}
}
public static class ShortFormContent extends DefaultSearchExtractorTest {

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.youtube.stream;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
@ -16,6 +17,7 @@ import java.util.List;
import javax.annotation.Nullable;
@Disabled("There is currently no way to extract age-restricted videos")
public class YoutubeStreamExtractorAgeRestrictedTest extends DefaultStreamExtractorTest {
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/stream/";
private static final String ID = "rwcfPqbAx-0";

View File

@ -43,7 +43,7 @@ public class YoutubeStreamExtractorControversialTest extends DefaultStreamExtrac
@Override public String expectedOriginalUrlContains() { return URL; }
@Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; }
@Override public String expectedUploaderName() { return "Amazing Atheist"; }
@Override public String expectedUploaderName() { return "INTO THE FRAY"; }
@Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCjNxszyFPasDdRoD9J6X-sw"; }
@Override public long expectedUploaderSubscriberCountAtLeast() { return 900_000; }
@Override public List<String> expectedDescriptionContains() {

View File

@ -68,14 +68,14 @@ public class YoutubeStreamExtractorDefaultTest {
public static final String YOUTUBE_LICENCE = "YouTube licence";
public static class NotAvailable {
@BeforeAll
public static void setUp() throws IOException {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "notAvailable"));
}
private static final String RESOURCE_PATH =
YoutubeStreamExtractorDefaultTest.RESOURCE_PATH + "notAvailable/";
@Test
void geoRestrictedContent() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "restricted"));
final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "_PL2HJKxnOM");
assertThrows(GeographicRestrictionException.class, extractor::fetchPage);
@ -83,6 +83,9 @@ public class YoutubeStreamExtractorDefaultTest {
@Test
void nonExistentFetch() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "nonExistent"));
final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "don-t-exist");
assertThrows(ContentNotAvailableException.class, extractor::fetchPage);
@ -90,6 +93,9 @@ public class YoutubeStreamExtractorDefaultTest {
@Test
void invalidId() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "invalidId"));
final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "INVALID_ID_INVALID_ID");
assertThrows(ParsingException.class, extractor::fetchPage);
@ -97,6 +103,9 @@ public class YoutubeStreamExtractorDefaultTest {
@Test
void paidContent() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "paidContent"));
final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "ayI2iBwGdxw");
assertThrows(PaidContentException.class, extractor::fetchPage);
@ -104,6 +113,9 @@ public class YoutubeStreamExtractorDefaultTest {
@Test
void privateContent() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "privateContent"));
final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "8VajtrESJzA");
assertThrows(PrivateContentException.class, extractor::fetchPage);
@ -111,6 +123,9 @@ public class YoutubeStreamExtractorDefaultTest {
@Test
void youtubeMusicPremiumContent() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicPremiumContent"));
final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "sMJ8bRN2dak");
assertThrows(YoutubeMusicPremiumContentException.class, extractor::fetchPage);

View File

@ -1,18 +0,0 @@
package org.schabi.newpipe.extractor.utils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ProtoBuilderTest {
@Test
public void testProtoBuilder() {
final ProtoBuilder pb = new ProtoBuilder();
pb.varint(1, 128);
pb.varint(2, 1234567890);
pb.varint(3, 1234567890123456789L);
pb.string(4, "Hello");
pb.bytes(5, new byte[]{1, 2, 3});
assertEquals("CIABENKF2MwEGJWCpu_HnoSRESIFSGVsbG8qAwECAw%3D%3D", pb.toUrlencodedBase64());
}
}

View File

@ -44,10 +44,10 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Sun, 10 Nov 2024 17:58:42 GMT"
"Wed, 12 Feb 2025 20:54:00 GMT"
],
"expires": [
"Sun, 10 Nov 2024 17:58:42 GMT"
"Wed, 12 Feb 2025 20:54:00 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
@ -65,8 +65,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003d8brHwG9jSy4; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 14-Feb-2022 17:58:42 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dmDWZXj-Vobk; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 19-May-2022 20:54:00 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -0,0 +1,87 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"https://www.youtube.com"
],
"Origin": [
"https://www.youtube.com"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"access-control-allow-credentials": [
"true"
],
"access-control-allow-origin": [
"https://www.youtube.com"
],
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Tue, 11 Feb 2025 21:26:02 GMT"
],
"expires": [
"Tue, 11 Feb 2025 21:26:02 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003d_I6wGtLOmJs; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 18-May-2022 21:26:02 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "\n self.addEventListener(\u0027install\u0027, event \u003d\u003e {\n event.waitUntil(self.skipWaiting());\n });\n self.addEventListener(\u0027activate\u0027, event \u003d\u003e {\n event.waitUntil(\n self.clients.claim().then(() \u003d\u003e self.registration.unregister()));\n });\n ",
"latestUrl": "https://www.youtube.com/sw.js"
}
}

View File

@ -0,0 +1,324 @@
{
"request": {
"httpMethod": "POST",
"url": "https://www.youtube.com/youtubei/v1/visitor_id?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"https://www.youtube.com"
],
"Origin": [
"https://www.youtube.com"
],
"Cookie": [
"SOCS\u003dCAE\u003d"
],
"X-YouTube-Client-Version": [
"2.20250210.02.00"
],
"X-YouTube-Client-Name": [
"1"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
99,
108,
105,
101,
110,
116,
83,
99,
114,
101,
101,
110,
34,
58,
34,
87,
65,
84,
67,
72,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
50,
46,
50,
48,
50,
53,
48,
50,
49,
48,
46,
48,
50,
46,
48,
48,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:26:03 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtBVVlNdGNaUXVvMCjrgK-9BjIKCgJERRIEEgAgHQ%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20250210.02.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetVisitorId_rid\",\"value\":\"0xaa026f1cc36208bd\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"visitor_data\",\"value\":\"CgtBVVlNdGNaUXVvMCjrgK-9BjIKCgJERRIEEgAgHQ%3D%3D\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20250210\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}}}",
"latestUrl": "https://www.youtube.com/youtubei/v1/visitor_id?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Tue, 11 Feb 2025 21:26:05 GMT"
],
"expires": [
"Tue, 11 Feb 2025 21:26:05 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003daF72zm975Ks; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d-F9WHpkngmo; Domain\u003d.youtube.com; Expires\u003dSun, 10-Aug-2025 21:26:05 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgOg%3D%3D; Domain\u003d.youtube.com; Expires\u003dSun, 10-Aug-2025 21:26:05 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCKrs3vud77f8PhDk38bHx7yLAxjk38bHx7yLAw%3D%3D; Domain\u003dyoutube.com; Expires\u003dSun, 10-Aug-2025 21:26:05 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/af7f576f\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;window[\u0027yt_embedsEnableAutoplayAndVisibilitySignals\u0027] \u003d true ;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,369 @@
{
"request": {
"httpMethod": "POST",
"url": "https://youtubei.googleapis.com/youtubei/v1/visitor_id?prettyPrint\u003dfalse",
"headers": {
"User-Agent": [
"com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
],
"X-Goog-Api-Format-Version": [
"2"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
97,
110,
100,
114,
111,
105,
100,
83,
100,
107,
86,
101,
114,
115,
105,
111,
110,
34,
58,
51,
53,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
44,
34,
111,
115,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
53,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
65,
78,
68,
82,
79,
73,
68,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
99,
108,
105,
101,
110,
116,
83,
99,
114,
101,
101,
110,
34,
58,
34,
87,
65,
84,
67,
72,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
57,
46,
50,
56,
46,
51,
53,
34,
44,
34,
111,
115,
78,
97,
109,
101,
34,
58,
34,
65,
110,
100,
114,
111,
105,
100,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
77,
79,
66,
73,
76,
69,
34,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:26:08 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgt6RmJ4SGNjMkp4cyjwgK-9BjIKCgJERRIEEgAgGToMCAEgwp71rYOO8NVn\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"ANDROID\"},{\"key\":\"cver\",\"value\":\"19.28.35\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetVisitorId_rid\",\"value\":\"0xbeaa9019cf98b3bf\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"visitor_data\",\"value\":\"Cgt6RmJ4SGNjMkp4cyjwgK-9BjIKCgJERRIEEgAgGToMCAEgwp71rYOO8NVn\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"19.28\"},{\"key\":\"client.name\",\"value\":\"ANDROID\"}]}]}}",
"latestUrl": "https://youtubei.googleapis.com/youtubei/v1/visitor_id?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Wed, 12 Feb 2025 20:47:12 GMT"
],
"expires": [
"Wed, 12 Feb 2025 20:47:12 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dLSDP0xuw9oM; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003dYUDo12GY-Ns; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:12 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgVQ%3D%3D; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:12 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCOLR6u6oxIKUYRD8rIXegL-LAxj8rIXegL-LAw%3D%3D; Domain\u003dyoutube.com; Expires\u003dMon, 11-Aug-2025 20:47:12 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/e7567ecf\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Wed, 12 Feb 2025 20:47:13 GMT"
],
"expires": [
"Wed, 12 Feb 2025 20:47:13 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dtzEnsk-eXCk; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCPPNh_3Pir_-bBDG_szegL-LAxjG_szegL-LAw%3D%3D; Domain\u003dyoutube.com; Expires\u003dMon, 11-Aug-2025 20:47:13 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned",
"VISITOR_INFO1_LIVE\u003dsfuOn2eMIeg; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:13 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgVQ%3D%3D; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:13 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/074a8365\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Wed, 12 Feb 2025 20:47:15 GMT"
],
"expires": [
"Wed, 12 Feb 2025 20:47:15 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dJm83U0hjoJA; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCLa4gJSBq9TnYBC4oJnfgL-LAxi4oJnfgL-LAw%3D%3D; Domain\u003dyoutube.com; Expires\u003dMon, 11-Aug-2025 20:47:15 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned",
"VISITOR_INFO1_LIVE\u003dMdPCxAs6k6s; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:15 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgPQ%3D%3D; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:15 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/074a8365\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027;report-uri /cspreport"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Wed, 12 Feb 2025 20:47:16 GMT"
],
"expires": [
"Wed, 12 Feb 2025 20:47:16 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dkeWCa58A8nU; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d-PZ3HazfI44; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgHw%3D%3D; Domain\u003d.youtube.com; Expires\u003dMon, 11-Aug-2025 20:47:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCLeNmcyo2bTTqQEQiszl34C_iwMYiszl34C_iwM%3D; Domain\u003dyoutube.com; Expires\u003dMon, 11-Aug-2025 20:47:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/074a8365\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Origin": [
"https://music.youtube.com"
],
"Referer": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:26:39 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtkdjdXeTVtWWhnVSiOga-9BjIKCgJERRIEEgAgCw%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0x0072d572126ab403\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"9405960,23804281,24004644,24077241,24181174,24241378,24290153,24439361,24548629,24566687,39328397,51010235,51020570,51025415,51050361,51053689,51063643,51065188,51089007,51098299,51101169,51111738,51115184,51124104,51152050,51157411,51176511,51183909,51204329,51217504,51222973,51224491,51225393,51227037,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51274583,51276557,51276565,51281227,51285717,51287196,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51305839,51310742,51313109,51313767,51313802,51318845,51322669,51326932,51333103,51335366,51335392,51340662,51341975,51342753,51342857,51349880,51349913,51351446,51353231,51353393,51354114,51355679,51357477,51361727,51362455,51362643,51362857,51362912,51365459,51365462,51366423,51366864,51367993,51370095,51371521,51372971,51374017,51374438,51375168,51375205,51375519,51375719,51376330,51376601,51379274,51380892,51381276,51381857,51383376,51384888,51385023,51385277,51386141,51386500,51386566,51387927,51388643,51388999,51389290,51389313,51389629,51389867,51391255,51391339,51395384,51395558,51396626,51398682,51399239,51399885,51400654,51400810,51404699,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwiW7tfXx7yLAxXDgHwGHUDYJys\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Origin": [
"https://music.youtube.com"
],
"Referer": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:26:47 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtKM21wZDJPQXdwdyiXga-9BjIKCgJERRIEEgAgJQ%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0xd605de9624bc897e\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,24004644,24077241,24181174,24186126,24241378,24290153,24428948,24439361,24499533,24548629,24566687,39328397,51010235,51020570,51025415,51043774,51050361,51053689,51063643,51065188,51089007,51098299,51111738,51115184,51124104,51152050,51157411,51176421,51176511,51183910,51204329,51217504,51222973,51224489,51225393,51227037,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51274583,51276557,51276565,51281227,51285717,51287196,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51305839,51310742,51313109,51313767,51313802,51318845,51322669,51326932,51335366,51335392,51340662,51341975,51342753,51342857,51342877,51349880,51349914,51351446,51353231,51353393,51354114,51355679,51357475,51357477,51361727,51362455,51362643,51362857,51362911,51365459,51365462,51366423,51366864,51367993,51370095,51371521,51372971,51374016,51374438,51375168,51375205,51375518,51375646,51375719,51376330,51376601,51379274,51380892,51381276,51381857,51383376,51384888,51385023,51385277,51385397,51386141,51386361,51386500,51386541,51386565,51387927,51388642,51388999,51389313,51389629,51389867,51391338,51395383,51395558,51396626,51398682,51399239,51399861,51399885,51400654,51400717,51400810,51404699,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwi9-c_bx7yLAxWqIAYAHXs5GWY\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"https://music.youtube.com"
],
"Origin": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:26:57 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtoOGhCeHl0dHE0NCigga-9BjIKCgJERRIEEgAgJw%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0xc2468fe509a23aa9\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,24004644,24077241,24181174,24186126,24241378,24290153,24439361,24548629,24566687,39328397,51010235,51020570,51025415,51037344,51037353,51050361,51053689,51063643,51065188,51089007,51098299,51101169,51111738,51115184,51124104,51152050,51157411,51176511,51178310,51178329,51178344,51178353,51183909,51204329,51217504,51222973,51225393,51227037,51227408,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51257940,51260454,51274583,51276557,51276565,51281227,51285717,51287196,51288520,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51305494,51305839,51311029,51311034,51313109,51313767,51313802,51316746,51318845,51322669,51323298,51326932,51327142,51327169,51327186,51329144,51331481,51331502,51331516,51331535,51331538,51331549,51331554,51331559,51333543,51335364,51335392,51335644,51335973,51340613,51340662,51341226,51341975,51342753,51342857,51349440,51349880,51349914,51351446,51353231,51353393,51354114,51354569,51355262,51355271,51355289,51355303,51355316,51355337,51355344,51355679,51355802,51357436,51357477,51360110,51360121,51360134,51361727,51361828,51362073,51362455,51362643,51362857,51362912,51363727,51363738,51363743,51363756,51363765,51363770,51364289,51365459,51365462,51366423,51366864,51367487,51367993,51369889,51370095,51370997,51371006,51371521,51372971,51374017,51374438,51375168,51375205,51375519,51375719,51376330,51376396,51376601,51379274,51380370,51380385,51380400,51380759,51380770,51380781,51380796,51380805,51380818,51380821,51380894,51381276,51381857,51383376,51384305,51384837,51384888,51385023,51385277,51385569,51386141,51386500,51386565,51386625,51387927,51388642,51388999,51389313,51389629,51389867,51390511,51391267,51391338,51391451,51393208,51394774,51394783,51395384,51395558,51395650,51395957,51396626,51397087,51398682,51398887,51399240,51399469,51399886,51400654,51400717,51400810,51401646,51402010,51402825,51403029,51404699,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwjQ8aHgx7yLAxV1CQYAHewBO1E\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"https://music.youtube.com"
],
"Origin": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:27:06 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtiRTZSbnJhNVFqYyiqga-9BjIKCgJERRIEEgAgSw%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0xac22c047248d13a0\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"9405960,23804281,23813628,24004644,24077241,24181174,24186125,24241378,24290153,24439361,24548629,24566687,39328397,51010235,51020570,51025415,51037344,51037353,51050361,51053689,51063643,51065188,51089007,51098299,51101169,51111738,51115184,51124104,51152050,51153492,51157411,51176511,51178316,51178337,51178340,51178351,51183910,51204329,51217504,51222973,51225393,51227037,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51260454,51274583,51276557,51276565,51281227,51285717,51287196,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51304729,51305031,51305494,51305839,51308708,51310741,51311025,51311040,51313109,51313767,51313802,51318845,51322669,51326932,51327144,51327167,51327178,51331483,51331504,51331516,51331529,51331540,51331545,51331554,51331561,51335366,51335392,51335644,51339142,51340611,51340662,51341226,51341975,51342753,51342857,51349880,51349913,51351446,51353231,51353393,51354114,51354567,51355268,51355271,51355289,51355303,51355316,51355339,51355342,51355679,51357477,51359177,51360106,51360125,51360138,51361727,51361828,51361846,51362040,51362073,51362455,51362643,51362857,51362911,51363725,51363736,51363741,51363754,51363763,51363772,51364291,51365459,51365462,51366423,51366864,51367489,51367993,51369905,51370095,51371003,51371006,51371521,51372971,51374016,51374438,51375074,51375168,51375205,51375518,51375719,51376330,51376601,51379211,51379274,51380372,51380383,51380400,51380763,51380772,51380785,51380788,51380807,51380816,51380827,51380894,51381276,51381857,51383376,51383439,51384306,51384888,51385023,51386141,51386500,51386540,51386548,51386566,51387927,51388643,51388999,51389313,51389629,51389867,51390075,51390512,51391127,51391339,51393143,51394776,51394783,51395383,51395558,51395649,51396626,51397089,51397104,51397282,51398392,51398608,51398682,51399240,51399885,51400655,51400810,51402011,51403173,51404699,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwi-t93kx7yLAxV9OQYAHSqgMoI\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Referer": [
"https://music.youtube.com"
],
"Origin": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:27:14 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgtab3c3RGJCZnZ2TSiyga-9BjIKCgJERRIEEgAgRQ%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0x6fa27aded42d1916\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,24004644,24077241,24181174,24241378,24290153,24439361,24548629,24566687,39328397,51010235,51020570,51025415,51050361,51053689,51063643,51065188,51089007,51098299,51111738,51115184,51124104,51134507,51152050,51157411,51176511,51183910,51204329,51217504,51222973,51224492,51225393,51227037,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51274583,51276557,51276565,51281227,51285717,51287196,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51305839,51310741,51313109,51313767,51313802,51318845,51322669,51326932,51334295,51335365,51335392,51340662,51341975,51342753,51342857,51349880,51349913,51351446,51353231,51353393,51354114,51355679,51357477,51361727,51362455,51362643,51362857,51362911,51365459,51365462,51366423,51366864,51367993,51370095,51371521,51372971,51374017,51374438,51375168,51375205,51375518,51375646,51375719,51376050,51376330,51376601,51379055,51379274,51380894,51381276,51381857,51383376,51384888,51385023,51385277,51385398,51386141,51386500,51386566,51387927,51388642,51388999,51389313,51389629,51389867,51391267,51391339,51395383,51395558,51396626,51398682,51399240,51399886,51400153,51400655,51400810,51403122,51404700,51405825,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwja8NHox7yLAxXcBAYAHVSMAsw\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Origin": [
"https://music.youtube.com"
],
"Referer": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:27:22 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtkNHo3aXhMd3dENCi6ga-9BjIKCgJERRIEEgAgWw%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0xa304b3b374db0b76\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,24004644,24077241,24181174,24241378,24290153,24407444,24439361,24459435,24548629,24566687,39328397,51010235,51020570,51025415,51037344,51037349,51050361,51053689,51063643,51065188,51089007,51098299,51111738,51115184,51124104,51146014,51152050,51157411,51157841,51176511,51178320,51178333,51178348,51178351,51183909,51203015,51204329,51217504,51222973,51225393,51227037,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51260454,51274583,51276557,51276565,51277311,51281227,51285717,51287196,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51305839,51308709,51310742,51311025,51311038,51313109,51313767,51313802,51316749,51316844,51318845,51322669,51326932,51327142,51327169,51327182,51331485,51331504,51331520,51331533,51331542,51331549,51331552,51331561,51333541,51335364,51335392,51335644,51337456,51340611,51340662,51341228,51341975,51342753,51342857,51349913,51351446,51353231,51353393,51354114,51354567,51355264,51355277,51355289,51355305,51355312,51355335,51355346,51355679,51357477,51360095,51360102,51360119,51360134,51361727,51361828,51362073,51362455,51362643,51362857,51362911,51363727,51363732,51363741,51363754,51363765,51363772,51364291,51365459,51365462,51366423,51366864,51367487,51367993,51369398,51370095,51370997,51371010,51371521,51372971,51374017,51374438,51375076,51375168,51375205,51375518,51375719,51376330,51376601,51379054,51379211,51379274,51380372,51380389,51380396,51380745,51380755,51380770,51380785,51380788,51380805,51380814,51380827,51380894,51381276,51381857,51383376,51384306,51384888,51385023,51385278,51386141,51386361,51386500,51386566,51387195,51387927,51388250,51388642,51388999,51389313,51389583,51389629,51389867,51390948,51391088,51391339,51393068,51394772,51394779,51395384,51395551,51395558,51395564,51395650,51395904,51395957,51396626,51397102,51397282,51398391,51398682,51399239,51399885,51400654,51400810,51401648,51402011,51403173,51404410,51404699,51405825,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwicpKrsx7yLAxU5ekECHVanKoc\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,315 @@
{
"request": {
"httpMethod": "POST",
"url": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse",
"headers": {
"Origin": [
"https://music.youtube.com"
],
"Referer": [
"https://music.youtube.com"
],
"X-YouTube-Client-Version": [
"2.20250122.04.00"
],
"X-YouTube-Client-Name": [
"67"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
87,
69,
66,
95,
82,
69,
77,
73,
88,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
46,
50,
48,
50,
53,
48,
49,
50,
50,
46,
48,
49,
46,
48,
48,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
68,
69,
83,
75,
84,
79,
80,
34,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
125,
44,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
44,
34,
105,
110,
112,
117,
116,
34,
58,
34,
34,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:27:30 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgt5RzRaNjZHWVVkNCjCga-9BjIKCgJERRIEEgAgYw%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB_REMIX\"},{\"key\":\"cver\",\"value\":\"1.20250122.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetMusicSearchSuggestions_rid\",\"value\":\"0xd8e0611239b12c0b\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23804281,24004644,24077241,24181174,24241378,24290153,24439361,24548629,24566687,39328397,51010235,51020570,51025415,51050361,51053689,51063643,51065188,51089007,51098299,51111738,51115184,51124104,51152050,51157411,51176511,51183910,51204329,51217504,51222973,51224492,51225393,51227037,51228850,51237842,51242448,51248734,51255676,51256074,51256084,51274583,51276557,51276565,51281227,51285717,51287196,51292055,51296439,51298020,51299710,51299724,51303667,51303669,51303789,51305839,51308708,51310742,51313109,51313767,51313802,51318845,51322669,51326932,51335364,51335392,51340662,51341975,51342753,51342857,51349880,51349914,51351446,51353231,51353393,51354114,51355679,51357474,51357477,51361727,51362455,51362643,51362857,51362912,51365459,51365462,51366423,51366864,51367993,51370095,51371521,51372971,51374016,51374438,51375168,51375205,51375519,51375719,51376330,51376601,51379054,51379274,51380894,51381276,51381857,51383376,51384888,51385023,51385398,51386141,51386500,51386566,51387927,51388642,51388999,51389313,51389629,51389867,51391256,51391339,51395383,51395558,51395903,51396626,51398391,51398682,51399240,51399886,51400152,51400655,51400716,51400765,51400810,51403122,51403173,51404699,51406269\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"1.20000101\"},{\"key\":\"client.name\",\"value\":\"WEB_REMIX\"}]}]},\"trackingParams\":\"CAAQi24iEwity4Xwx7yLAxVIQkECHU69JCw\u003d\"}",
"latestUrl": "https://music.youtube.com/youtubei/v1/music/get_search_suggestions?prettyPrint\u003dfalse"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Tue, 11 Feb 2025 21:20:44 GMT"
],
"expires": [
"Tue, 11 Feb 2025 21:20:44 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003d-HZn8W-65YI; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCNz89uH5v5DP1AEQ5bPQrsa8iwMY5bPQrsa8iwM%3D; Domain\u003dyoutube.com; Expires\u003dSun, 10-Aug-2025 21:20:44 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned",
"VISITOR_INFO1_LIVE\u003drMJck_iy7Ks; Domain\u003d.youtube.com; Expires\u003dSun, 10-Aug-2025 21:20:44 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgEA%3D%3D; Domain\u003d.youtube.com; Expires\u003dSun, 10-Aug-2025 21:20:44 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/af7f576f\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,80 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Tue, 11 Feb 2025 21:26:31 GMT"
],
"expires": [
"Tue, 11 Feb 2025 21:26:31 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003d_RnQFljRZ7g; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003dFTq2gAjABPk; Domain\u003d.youtube.com; Expires\u003dSun, 10-Aug-2025 21:26:31 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJERRIEEgAgMg%3D%3D; Domain\u003d.youtube.com; Expires\u003dSun, 10-Aug-2025 21:26:31 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"__Secure-ROLLOUT_TOKEN\u003dCKf_-fLDg_2hpwEQz7H508e8iwMYz7H508e8iwM%3D; Domain\u003dyoutube.com; Expires\u003dSun, 10-Aug-2025 21:26:31 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone; Partitioned"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/af7f576f\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;var i\u003d0;for(;i\u003cl.length;i++)try{l[i]()}catch(e){}};YT.setConfig\u003dfunction(c){var k;for(k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",\nn)}var b\u003ddocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -35,7 +35,7 @@
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
"require-trusted-types-for \u0027script\u0027;report-uri /cspreport"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
@ -44,10 +44,10 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Sun, 10 Nov 2024 17:57:16 GMT"
"Tue, 11 Feb 2025 21:27:51 GMT"
],
"expires": [
"Sun, 10 Nov 2024 17:57:16 GMT"
"Tue, 11 Feb 2025 21:27:51 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
@ -65,8 +65,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dB6Ez5rWmaxs; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 14-Feb-2022 17:57:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dYHvWgExoeiY; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 18-May-2022 21:27:51 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -0,0 +1,369 @@
{
"request": {
"httpMethod": "POST",
"url": "https://youtubei.googleapis.com/youtubei/v1/visitor_id?prettyPrint\u003dfalse",
"headers": {
"User-Agent": [
"com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
],
"X-Goog-Api-Format-Version": [
"2"
],
"Content-Type": [
"application/json"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"dataToSend": [
123,
34,
99,
111,
110,
116,
101,
120,
116,
34,
58,
123,
34,
114,
101,
113,
117,
101,
115,
116,
34,
58,
123,
34,
105,
110,
116,
101,
114,
110,
97,
108,
69,
120,
112,
101,
114,
105,
109,
101,
110,
116,
70,
108,
97,
103,
115,
34,
58,
91,
93,
44,
34,
117,
115,
101,
83,
115,
108,
34,
58,
116,
114,
117,
101,
125,
44,
34,
99,
108,
105,
101,
110,
116,
34,
58,
123,
34,
97,
110,
100,
114,
111,
105,
100,
83,
100,
107,
86,
101,
114,
115,
105,
111,
110,
34,
58,
51,
53,
44,
34,
117,
116,
99,
79,
102,
102,
115,
101,
116,
77,
105,
110,
117,
116,
101,
115,
34,
58,
48,
44,
34,
111,
115,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
53,
34,
44,
34,
104,
108,
34,
58,
34,
101,
110,
45,
71,
66,
34,
44,
34,
99,
108,
105,
101,
110,
116,
78,
97,
109,
101,
34,
58,
34,
65,
78,
68,
82,
79,
73,
68,
34,
44,
34,
103,
108,
34,
58,
34,
71,
66,
34,
44,
34,
99,
108,
105,
101,
110,
116,
83,
99,
114,
101,
101,
110,
34,
58,
34,
87,
65,
84,
67,
72,
34,
44,
34,
99,
108,
105,
101,
110,
116,
86,
101,
114,
115,
105,
111,
110,
34,
58,
34,
49,
57,
46,
50,
56,
46,
51,
53,
34,
44,
34,
111,
115,
78,
97,
109,
101,
34,
58,
34,
65,
110,
100,
114,
111,
105,
100,
34,
44,
34,
112,
108,
97,
116,
102,
111,
114,
109,
34,
58,
34,
77,
79,
66,
73,
76,
69,
34,
125,
44,
34,
117,
115,
101,
114,
34,
58,
123,
34,
108,
111,
99,
107,
101,
100,
83,
97,
102,
101,
116,
121,
77,
111,
100,
101,
34,
58,
102,
97,
108,
115,
101,
125,
125,
125
],
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"content-type": [
"application/json; charset\u003dUTF-8"
],
"date": [
"Tue, 11 Feb 2025 21:27:56 GMT"
],
"server": [
"scaffolding on HTTPServer2"
],
"vary": [
"Origin",
"X-Origin",
"Referer"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtqdG01NHlwS3c0TSjcga-9BjIKCgJERRIEEgAgWToMCAEgwazilcKb8NVn\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"ANDROID\"},{\"key\":\"cver\",\"value\":\"19.28.35\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetVisitorId_rid\",\"value\":\"0x3a567781fcbf0a17\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"visitor_data\",\"value\":\"CgtqdG01NHlwS3c0TSjcga-9BjIKCgJERRIEEgAgWToMCAEgwazilcKb8NVn\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"19.28\"},{\"key\":\"client.name\",\"value\":\"ANDROID\"}]}]}}",
"latestUrl": "https://youtubei.googleapis.com/youtubei/v1/visitor_id?prettyPrint\u003dfalse"
}
}

View File

@ -44,10 +44,10 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Sun, 10 Nov 2024 17:57:17 GMT"
"Tue, 11 Feb 2025 21:28:02 GMT"
],
"expires": [
"Sun, 10 Nov 2024 17:57:17 GMT"
"Tue, 11 Feb 2025 21:28:02 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
@ -65,8 +65,8 @@
"ESF"
],
"set-cookie": [
"YSC\u003dxeWWSmImBKY; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 14-Feb-2022 17:57:17 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
"YSC\u003dPp3KL5JdAzE; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 18-May-2022 21:28:02 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