Merge branch 'dev' of github.com:TeamNewPipe/NewPipeExtractor into channel-tabs
This commit is contained in:
commit
9cebcf7ab6
|
@ -15,10 +15,10 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: set up JDK 8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '8'
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
|
|
|
@ -8,7 +8,7 @@ allprojects {
|
|||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
version 'v0.22.1'
|
||||
version 'v0.22.5'
|
||||
group 'com.github.TeamNewPipe'
|
||||
|
||||
repositories {
|
||||
|
@ -29,7 +29,7 @@ allprojects {
|
|||
ext {
|
||||
nanojsonVersion = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||
spotbugsVersion = "4.7.3"
|
||||
junitVersion = "5.9.1"
|
||||
junitVersion = "5.9.2"
|
||||
checkstyleVersion = "10.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ dependencies {
|
|||
implementation project(':timeago-parser')
|
||||
|
||||
implementation "com.github.TeamNewPipe:nanojson:$nanojsonVersion"
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.4'
|
||||
implementation "com.github.spotbugs:spotbugs-annotations:$spotbugsVersion"
|
||||
|
||||
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||
|
@ -41,5 +41,5 @@ dependencies {
|
|||
testImplementation 'org.junit.jupiter:junit-jupiter-params'
|
||||
|
||||
testImplementation "com.squareup.okhttp3:okhttp:3.12.13"
|
||||
testImplementation 'com.google.code.gson:gson:2.10'
|
||||
testImplementation 'com.google.code.gson:gson:2.10.1'
|
||||
}
|
||||
|
|
|
@ -22,6 +22,13 @@ public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem>
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the total number of comments
|
||||
*/
|
||||
public int getCommentsCount() throws ExtractionException {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
|
|
|
@ -48,6 +48,11 @@ public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
|||
ExtractorHelper.getItemsPageOrLogError(commentsInfo, commentsExtractor);
|
||||
commentsInfo.setCommentsDisabled(commentsExtractor.isCommentsDisabled());
|
||||
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
|
||||
try {
|
||||
commentsInfo.setCommentsCount(commentsExtractor.getCommentsCount());
|
||||
} catch (final Exception e) {
|
||||
commentsInfo.addError(e);
|
||||
}
|
||||
commentsInfo.setNextPage(initialCommentsPage.getNextPage());
|
||||
|
||||
return commentsInfo;
|
||||
|
@ -76,6 +81,7 @@ public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
|||
|
||||
private transient CommentsExtractor commentsExtractor;
|
||||
private boolean commentsDisabled = false;
|
||||
private int commentsCount;
|
||||
|
||||
public CommentsExtractor getCommentsExtractor() {
|
||||
return commentsExtractor;
|
||||
|
@ -86,7 +92,6 @@ public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
|||
}
|
||||
|
||||
/**
|
||||
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||
* @return {@code true} if the comments are disabled otherwise {@code false} (default)
|
||||
* @see CommentsExtractor#isCommentsDisabled()
|
||||
*/
|
||||
|
@ -95,10 +100,27 @@ public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
|||
}
|
||||
|
||||
/**
|
||||
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||
* @param commentsDisabled {@code true} if the comments are disabled otherwise {@code false}
|
||||
*/
|
||||
public void setCommentsDisabled(final boolean commentsDisabled) {
|
||||
this.commentsDisabled = commentsDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of comments.
|
||||
*
|
||||
* @return the total number of comments
|
||||
*/
|
||||
public int getCommentsCount() {
|
||||
return commentsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of comments.
|
||||
*
|
||||
* @param commentsCount the commentsCount to set.
|
||||
*/
|
||||
public void setCommentsCount(final int commentsCount) {
|
||||
this.commentsCount = commentsCount;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import org.schabi.newpipe.extractor.localization.Localization;
|
|||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -39,7 +41,7 @@ public abstract class Downloader {
|
|||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the GET request
|
||||
*/
|
||||
public Response get(final String url, @Nullable final Localization localization)
|
||||
public Response get(final String url, final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return get(url, null, localization);
|
||||
}
|
||||
|
@ -70,7 +72,7 @@ public abstract class Downloader {
|
|||
*/
|
||||
public Response get(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final Localization localization)
|
||||
final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return execute(Request.newBuilder()
|
||||
.get(url)
|
||||
|
@ -112,7 +114,7 @@ public abstract class Downloader {
|
|||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @return the result of the GET request
|
||||
* @return the result of the POST request
|
||||
*/
|
||||
public Response post(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
|
@ -131,12 +133,12 @@ public abstract class Downloader {
|
|||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the GET request
|
||||
* @return the result of the POST request
|
||||
*/
|
||||
public Response post(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
@Nullable final Localization localization)
|
||||
final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return execute(Request.newBuilder()
|
||||
.post(url, dataToSend)
|
||||
|
@ -145,6 +147,95 @@ public abstract class Downloader {
|
|||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request using the specified value of the
|
||||
* {@code Content-Type} header with a given {@link Localization}.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @param contentType the mime type of the body sent, which will be set as the value of the
|
||||
* {@code Content-Type} header
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentType(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final Localization localization,
|
||||
final String contentType)
|
||||
throws IOException, ReCaptchaException {
|
||||
final Map<String, List<String>> actualHeaders = new HashMap<>();
|
||||
if (headers != null) {
|
||||
actualHeaders.putAll(headers);
|
||||
}
|
||||
actualHeaders.put("Content-Type", Collections.singletonList(contentType));
|
||||
return post(url, actualHeaders, dataToSend, localization);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request using the specified value of the
|
||||
* {@code Content-Type} header.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param contentType the mime type of the body sent, which will be set as the value of the
|
||||
* {@code Content-Type} header
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentType(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final String contentType)
|
||||
throws IOException, ReCaptchaException {
|
||||
return postWithContentType(url, headers, dataToSend, NewPipe.getPreferredLocalization(),
|
||||
contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request the JSON mime type as the value of the
|
||||
* {@code Content-Type} header with a given {@link Localization}.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentTypeJson(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return postWithContentType(url, headers, dataToSend, localization, "application/json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request the JSON mime type as the value of the
|
||||
* {@code Content-Type} header.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentTypeJson(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend)
|
||||
throws IOException, ReCaptchaException {
|
||||
return postWithContentTypeJson(url, headers, dataToSend,
|
||||
NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a request using the specified {@link Request} object.
|
||||
*
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package org.schabi.newpipe.extractor.localization;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
@ -14,6 +12,9 @@ import java.util.Locale;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class Localization implements Serializable {
|
||||
public static final Localization DEFAULT = new Localization("en", "GB");
|
||||
|
||||
|
@ -38,19 +39,7 @@ public class Localization implements Serializable {
|
|||
* @param localizationCode a localization code, formatted like {@link #getLocalizationCode()}
|
||||
*/
|
||||
public static Localization fromLocalizationCode(final String localizationCode) {
|
||||
final int indexSeparator = localizationCode.indexOf("-");
|
||||
|
||||
final String languageCode;
|
||||
final String countryCode;
|
||||
if (indexSeparator != -1) {
|
||||
languageCode = localizationCode.substring(0, indexSeparator);
|
||||
countryCode = localizationCode.substring(indexSeparator + 1);
|
||||
} else {
|
||||
languageCode = localizationCode;
|
||||
countryCode = null;
|
||||
}
|
||||
|
||||
return new Localization(languageCode, countryCode);
|
||||
return fromLocale(LocaleCompat.forLanguageTag(localizationCode));
|
||||
}
|
||||
|
||||
public Localization(@Nonnull final String languageCode, @Nullable final String countryCode) {
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||
|
@ -11,13 +15,22 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
|
||||
public class BandcampCommentsExtractor extends CommentsExtractor {
|
||||
|
||||
private static final String REVIEWS_API_URL = BASE_API_URL + "/tralbumcollectors/2/reviews";
|
||||
|
||||
private Document document;
|
||||
|
||||
|
||||
|
@ -39,18 +52,85 @@ public class BandcampCommentsExtractor extends CommentsExtractor {
|
|||
|
||||
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
|
||||
|
||||
final Elements writings = document.getElementsByClass("writing");
|
||||
final JsonObject collectorsData = JsonUtils.toJsonObject(
|
||||
document.getElementById("collectors-data").attr("data-blob"));
|
||||
final JsonArray reviews = collectorsData.getArray("reviews");
|
||||
|
||||
for (final Element writing : writings) {
|
||||
collector.commit(new BandcampCommentsInfoItemExtractor(writing, getUrl()));
|
||||
for (final Object review : reviews) {
|
||||
collector.commit(
|
||||
new BandcampCommentsInfoItemExtractor((JsonObject) review, getUrl()));
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
if (!collectorsData.getBoolean("more_reviews_available")) {
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
final String trackId = getTrackId();
|
||||
final String token = getNextPageToken(reviews);
|
||||
return new InfoItemsPage<>(collector, new Page(List.of(trackId, token)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return null;
|
||||
|
||||
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
|
||||
|
||||
final List<String> pageIds = page.getIds();
|
||||
final String trackId = pageIds.get(0);
|
||||
final String token = pageIds.get(1);
|
||||
final JsonObject reviewsData = fetchReviewsData(trackId, token);
|
||||
final JsonArray reviews = reviewsData.getArray("results");
|
||||
|
||||
for (final Object review : reviews) {
|
||||
collector.commit(
|
||||
new BandcampCommentsInfoItemExtractor((JsonObject) review, getUrl()));
|
||||
}
|
||||
|
||||
if (!reviewsData.getBoolean("more_available")) {
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector,
|
||||
new Page(List.of(trackId, getNextPageToken(reviews))));
|
||||
}
|
||||
|
||||
private JsonObject fetchReviewsData(final String trackId, final String token)
|
||||
throws ParsingException {
|
||||
try {
|
||||
return JsonUtils.toJsonObject(getDownloader().postWithContentTypeJson(
|
||||
REVIEWS_API_URL,
|
||||
Collections.emptyMap(),
|
||||
JsonWriter.string().object()
|
||||
.value("tralbum_type", "t")
|
||||
.value("tralbum_id", trackId)
|
||||
.value("token", token)
|
||||
.value("count", 7)
|
||||
.array("exclude_fan_ids").end()
|
||||
.end().done().getBytes(StandardCharsets.UTF_8)).responseBody());
|
||||
} catch (final IOException | ReCaptchaException e) {
|
||||
throw new ParsingException("Could not fetch reviews", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getNextPageToken(final JsonArray reviews) throws ParsingException {
|
||||
return reviews.stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.map(review -> review.getString("token"))
|
||||
.reduce((a, b) -> b) // keep only the last element
|
||||
.orElseThrow(() -> new ParsingException("Could not get token"));
|
||||
}
|
||||
|
||||
private String getTrackId() throws ParsingException {
|
||||
final JsonObject pageProperties = JsonUtils.toJsonObject(
|
||||
document.selectFirst("meta[name=bc-page-properties]")
|
||||
.attr("content"));
|
||||
return Long.toString(pageProperties.getLong("item_id"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCommentsDisabled() throws ExtractionException {
|
||||
return BandcampExtractorHelper.isRadioUrl(getUrl());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class BandcampCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
|
||||
|
||||
private final Element writing;
|
||||
private final JsonObject review;
|
||||
private final String url;
|
||||
|
||||
public BandcampCommentsInfoItemExtractor(final Element writing, final String url) {
|
||||
this.writing = writing;
|
||||
public BandcampCommentsInfoItemExtractor(final JsonObject review, final String url) {
|
||||
this.review = review;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
@ -29,31 +30,21 @@ public class BandcampCommentsInfoItemExtractor implements CommentsInfoItemExtrac
|
|||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return writing.getElementsByClass("thumb").attr("src");
|
||||
return getUploaderAvatarUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description getCommentText() throws ParsingException {
|
||||
final var text = writing.getElementsByClass("text").stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Element::ownText)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ParsingException("Could not get comment text"));
|
||||
|
||||
return new Description(text, Description.PLAIN_TEXT);
|
||||
return new Description(review.getString("why"), Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return writing.getElementsByClass("name").stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Element::text)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
|
||||
return review.getString("name");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return writing.getElementsByClass("thumb").attr("src");
|
||||
return getImageUrl(review.getLong("image_id"), false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,22 +6,23 @@ import com.grack.nanojson.JsonObject;
|
|||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class BandcampExtractorHelper {
|
||||
|
||||
public static final String BASE_URL = "https://bandcamp.com";
|
||||
|
@ -43,8 +44,8 @@ public final class BandcampExtractorHelper {
|
|||
+ "&tralbum_id=" + itemId + "&tralbum_type=" + itemType.charAt(0))
|
||||
.responseBody();
|
||||
|
||||
return JsonParser.object().from(jsonString)
|
||||
.getString("bandcamp_url").replace("http://", "https://");
|
||||
return Utils.replaceHttpWithHttps(JsonParser.object().from(jsonString)
|
||||
.getString("bandcamp_url"));
|
||||
|
||||
} catch (final JsonParserException | ReCaptchaException | IOException e) {
|
||||
throw new ParsingException("Ids could not be translated to URL", e);
|
||||
|
@ -60,19 +61,15 @@ public final class BandcampExtractorHelper {
|
|||
*/
|
||||
public static JsonObject getArtistDetails(final String id) throws ParsingException {
|
||||
try {
|
||||
return
|
||||
JsonParser.object().from(
|
||||
NewPipe.getDownloader().post(
|
||||
BASE_API_URL + "/mobile/22/band_details",
|
||||
null,
|
||||
JsonWriter.string()
|
||||
.object()
|
||||
.value("band_id", id)
|
||||
.end()
|
||||
.done()
|
||||
.getBytes()
|
||||
).responseBody()
|
||||
);
|
||||
return JsonParser.object().from(NewPipe.getDownloader().postWithContentTypeJson(
|
||||
BASE_API_URL + "/mobile/22/band_details",
|
||||
Collections.emptyMap(),
|
||||
JsonWriter.string()
|
||||
.object()
|
||||
.value("band_id", id)
|
||||
.end()
|
||||
.done()
|
||||
.getBytes(StandardCharsets.UTF_8)).responseBody());
|
||||
} catch (final IOException | ReCaptchaException | JsonParserException e) {
|
||||
throw new ParsingException("Could not download band details", e);
|
||||
}
|
||||
|
@ -123,7 +120,7 @@ public final class BandcampExtractorHelper {
|
|||
/**
|
||||
* Whether the URL points to a radio kiosk.
|
||||
* @param url the URL to check
|
||||
* @return true if the URL matches <code>https://bandcamp.com/?show=SHOW_ID</code>
|
||||
* @return true if the URL matches {@code https://bandcamp.com/?show=SHOW_ID}
|
||||
*/
|
||||
public static boolean isRadioUrl(final String url) {
|
||||
return url.toLowerCase().matches("https?://bandcamp\\.com/\\?show=\\d+");
|
||||
|
|
|
@ -18,6 +18,8 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
|||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||
|
||||
|
@ -40,11 +42,11 @@ public class BandcampFeaturedExtractor extends KioskExtractor<PlaylistInfoItem>
|
|||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
try {
|
||||
json = JsonParser.object().from(
|
||||
getDownloader().post(
|
||||
FEATURED_API_URL, null, "{\"platform\":\"\",\"version\":0}".getBytes()
|
||||
).responseBody()
|
||||
);
|
||||
json = JsonParser.object().from(getDownloader().postWithContentTypeJson(
|
||||
FEATURED_API_URL,
|
||||
Collections.emptyMap(),
|
||||
"{\"platform\":\"\",\"version\":0}".getBytes(StandardCharsets.UTF_8))
|
||||
.responseBody());
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ParsingException("Could not parse Bandcamp featured API response", e);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,10 @@ public class BandcampCommentsLinkHandlerFactory extends ListLinkHandlerFactory {
|
|||
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) throws ParsingException {
|
||||
if (BandcampExtractorHelper.isRadioUrl(url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't accept URLs that don't point to a track
|
||||
if (!url.toLowerCase().matches("https?://.+\\..+/(track|album)/.+")) {
|
||||
return false;
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
|||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -114,15 +115,24 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
|||
mediaFormat = null;
|
||||
}
|
||||
|
||||
// Not checking containsSimilarStream here, since MediaCCC does not provide enough
|
||||
// information to decide whether two streams are similar. Hence that method would
|
||||
// always return false, e.g. even for different language variations.
|
||||
audioStreams.add(new AudioStream.Builder()
|
||||
final AudioStream.Builder builder = new AudioStream.Builder()
|
||||
.setId(recording.getString("filename", ID_UNKNOWN))
|
||||
.setContent(recording.getString("recording_url"), true)
|
||||
.setMediaFormat(mediaFormat)
|
||||
.setAverageBitrate(UNKNOWN_BITRATE)
|
||||
.build());
|
||||
.setAverageBitrate(UNKNOWN_BITRATE);
|
||||
|
||||
final String language = recording.getString("language");
|
||||
// If the language contains a - symbol, this means that the stream has an audio
|
||||
// track with multiple languages, so there is no specific language for this stream
|
||||
// Don't set the audio language in this case
|
||||
if (language != null && !language.contains("-")) {
|
||||
builder.setAudioLocale(LocaleCompat.forLanguageTag(language));
|
||||
}
|
||||
|
||||
// Not checking containsSimilarStream here, since MediaCCC does not provide enough
|
||||
// information to decide whether two streams are similar. Hence that method would
|
||||
// always return false, e.g. even for different language variations.
|
||||
audioStreams.add(builder.build());
|
||||
}
|
||||
}
|
||||
return audioStreams;
|
||||
|
|
|
@ -2,10 +2,14 @@ package org.schabi.newpipe.extractor.services.peertube;
|
|||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeChannelInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubePlaylistInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeSepiaStreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeStreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
@ -101,10 +105,16 @@ public final class PeertubeParsingHelper {
|
|||
if (item.has("video")) {
|
||||
item = item.getObject("video");
|
||||
}
|
||||
final boolean isPlaylistInfoItem = item.has("videosLength");
|
||||
final boolean isChannelInfoItem = item.has("followersCount");
|
||||
|
||||
final PeertubeStreamInfoItemExtractor extractor;
|
||||
final InfoItemExtractor extractor;
|
||||
if (sepia) {
|
||||
extractor = new PeertubeSepiaStreamInfoItemExtractor(item, baseUrl);
|
||||
} else if (isPlaylistInfoItem) {
|
||||
extractor = new PeertubePlaylistInfoItemExtractor(item, baseUrl);
|
||||
} else if (isChannelInfoItem) {
|
||||
extractor = new PeertubeChannelInfoItemExtractor(item, baseUrl);
|
||||
} else {
|
||||
extractor = new PeertubeStreamInfoItemExtractor(item, baseUrl);
|
||||
}
|
||||
|
|
|
@ -1,50 +1,58 @@
|
|||
package org.schabi.newpipe.extractor.services.peertube.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class PeertubeChannelInfoItemExtractor implements ChannelInfoItemExtractor {
|
||||
|
||||
protected final JsonObject item;
|
||||
private final String baseUrl;
|
||||
|
||||
public PeertubeChannelInfoItemExtractor(final JsonObject item, final String baseUrl) {
|
||||
final JsonObject item;
|
||||
final JsonObject uploader;
|
||||
final String baseUrl;
|
||||
public PeertubeChannelInfoItemExtractor(@Nonnull final JsonObject item,
|
||||
@Nonnull final String baseUrl) {
|
||||
this.item = item;
|
||||
this.uploader = item.getObject("uploader");
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return item.getString("displayName");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return JsonUtils.getString(item, "url");
|
||||
return item.getString("url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
final JsonObject avatar = JsonUtils.getObject(item, "avatar");
|
||||
return baseUrl + JsonUtils.getString(avatar, "path");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return JsonUtils.getString(item, "displayName");
|
||||
return item.getArray("avatars").stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.max(Comparator.comparingInt(avatar -> avatar.getInt("width")))
|
||||
.map(avatar -> baseUrl + avatar.getString("path"))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() throws ParsingException {
|
||||
return item.getString("description", "");
|
||||
return item.getString("description");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSubscriberCount() throws ParsingException {
|
||||
return JsonUtils.getNumber(item, "followersCount").longValue();
|
||||
return item.getInt("followersCount");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() throws ParsingException {
|
||||
return ListExtractor.ITEM_COUNT_UNKNOWN;
|
||||
return ChannelExtractor.ITEM_COUNT_UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -26,6 +26,12 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|||
import javax.annotation.Nonnull;
|
||||
|
||||
public class PeertubeCommentsExtractor extends CommentsExtractor {
|
||||
|
||||
/**
|
||||
* Use {@link #isReply()} to access this variable.
|
||||
*/
|
||||
private Boolean isReply = null;
|
||||
|
||||
public PeertubeCommentsExtractor(final StreamingService service,
|
||||
final ListLinkHandler uiHandler) {
|
||||
super(service, uiHandler);
|
||||
|
@ -35,12 +41,27 @@ public class PeertubeCommentsExtractor extends CommentsExtractor {
|
|||
@Override
|
||||
public InfoItemsPage<CommentsInfoItem> getInitialPage()
|
||||
throws IOException, ExtractionException {
|
||||
return getPage(new Page(getUrl() + "?" + START_KEY + "=0&"
|
||||
+ COUNT_KEY + "=" + ITEMS_PER_PAGE));
|
||||
if (isReply()) {
|
||||
return getPage(new Page(getOriginalUrl()));
|
||||
} else {
|
||||
return getPage(new Page(getUrl() + "?" + START_KEY + "=0&"
|
||||
+ COUNT_KEY + "=" + ITEMS_PER_PAGE));
|
||||
}
|
||||
}
|
||||
|
||||
private void collectCommentsFrom(final CommentsInfoItemsCollector collector,
|
||||
final JsonObject json) throws ParsingException {
|
||||
private boolean isReply() throws ParsingException {
|
||||
if (isReply == null) {
|
||||
if (getOriginalUrl().contains("/videos/watch/")) {
|
||||
isReply = false;
|
||||
} else {
|
||||
isReply = getOriginalUrl().contains("/comment-threads/");
|
||||
}
|
||||
}
|
||||
return isReply;
|
||||
}
|
||||
|
||||
private void collectCommentsFrom(@Nonnull final CommentsInfoItemsCollector collector,
|
||||
@Nonnull final JsonObject json) throws ParsingException {
|
||||
final JsonArray contents = json.getArray("data");
|
||||
|
||||
for (final Object c : contents) {
|
||||
|
@ -53,6 +74,20 @@ public class PeertubeCommentsExtractor extends CommentsExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
private void collectRepliesFrom(@Nonnull final CommentsInfoItemsCollector collector,
|
||||
@Nonnull final JsonObject json) throws ParsingException {
|
||||
final JsonArray contents = json.getArray("children");
|
||||
|
||||
for (final Object c : contents) {
|
||||
if (c instanceof JsonObject) {
|
||||
final JsonObject item = ((JsonObject) c).getObject("comment");
|
||||
if (!item.getBoolean("isDeleted")) {
|
||||
collector.commit(new PeertubeCommentsInfoItemExtractor(item, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
|
@ -73,11 +108,17 @@ public class PeertubeCommentsExtractor extends CommentsExtractor {
|
|||
|
||||
if (json != null) {
|
||||
PeertubeParsingHelper.validate(json);
|
||||
final long total = json.getLong("total");
|
||||
|
||||
final long total;
|
||||
final CommentsInfoItemsCollector collector
|
||||
= new CommentsInfoItemsCollector(getServiceId());
|
||||
collectCommentsFrom(collector, json);
|
||||
|
||||
if (isReply() || json.has("children")) {
|
||||
total = json.getArray("children").size();
|
||||
collectRepliesFrom(collector, json);
|
||||
} else {
|
||||
total = json.getLong("total");
|
||||
collectCommentsFrom(collector, json);
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector,
|
||||
PeertubeParsingHelper.getNextPage(page.getUrl(), total));
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.grack.nanojson.JsonObject;
|
|||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
@ -12,6 +13,7 @@ import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
|||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
|
||||
|
@ -29,7 +31,7 @@ public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtrac
|
|||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return url;
|
||||
return url + "/" + getCommentId();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -101,4 +103,19 @@ public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtrac
|
|||
return ServiceList.PeerTube.getChannelLHFactory()
|
||||
.fromId("accounts/" + name + "@" + host, baseUrl).getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Page getReplies() throws ParsingException {
|
||||
if (JsonUtils.getNumber(item, "totalReplies").intValue() == 0) {
|
||||
return null;
|
||||
}
|
||||
final String threadId = JsonUtils.getNumber(item, "threadId").toString();
|
||||
return new Page(url + "/" + threadId, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReplyCount() throws ParsingException {
|
||||
return JsonUtils.getNumber(item, "totalReplies").intValue();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,57 @@
|
|||
package org.schabi.newpipe.extractor.services.peertube.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class PeertubePlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||
public class PeertubePlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||
|
||||
protected final JsonObject item;
|
||||
private final String baseUrl;
|
||||
final JsonObject item;
|
||||
final JsonObject uploader;
|
||||
final String baseUrl;
|
||||
|
||||
public PeertubePlaylistInfoItemExtractor(final JsonObject item, final String baseUrl) {
|
||||
public PeertubePlaylistInfoItemExtractor(@Nonnull final JsonObject item,
|
||||
@Nonnull final String baseUrl) {
|
||||
this.item = item;
|
||||
this.uploader = item.getObject("uploader");
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return item.getString("displayName");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
final String uuid = JsonUtils.getString(item, "shortUUID");
|
||||
return baseUrl + "/w/p/" + uuid;
|
||||
return item.getString("url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return baseUrl + JsonUtils.getString(item, "thumbnailPath");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return JsonUtils.getString(item, "displayName");
|
||||
return baseUrl + item.getString("thumbnailPath");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
final JsonObject owner = JsonUtils.getObject(item, "ownerAccount");
|
||||
return JsonUtils.getString(owner, "displayName");
|
||||
return uploader.getString("displayName");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() throws ParsingException {
|
||||
final JsonObject owner = JsonUtils.getObject(item, "ownerAccount");
|
||||
return JsonUtils.getString(owner, "url");
|
||||
return uploader.getString("url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() {
|
||||
public boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() throws ParsingException {
|
||||
return JsonUtils.getNumber(item, "videosLength").longValue();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
|
||||
return PlaylistInfoItemExtractor.super.getPlaylistType();
|
||||
return item.getInt("videosLength");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
private final List<AudioStream> audioStreams = new ArrayList<>();
|
||||
private final List<VideoStream> videoStreams = new ArrayList<>();
|
||||
|
||||
private ParsingException subtitlesException = null;
|
||||
|
||||
public PeertubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler)
|
||||
throws ParsingException {
|
||||
super(service, linkHandler);
|
||||
|
@ -262,13 +264,19 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<SubtitlesStream> getSubtitlesDefault() {
|
||||
public List<SubtitlesStream> getSubtitlesDefault() throws ParsingException {
|
||||
if (subtitlesException != null) {
|
||||
throw subtitlesException;
|
||||
}
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<SubtitlesStream> getSubtitles(final MediaFormat format) {
|
||||
public List<SubtitlesStream> getSubtitles(final MediaFormat format) throws ParsingException {
|
||||
if (subtitlesException != null) {
|
||||
throw subtitlesException;
|
||||
}
|
||||
return subtitles.stream()
|
||||
.filter(sub -> sub.getFormat() == format)
|
||||
.collect(Collectors.toList());
|
||||
|
@ -321,7 +329,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
@Nonnull
|
||||
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
|
||||
throws UnsupportedEncodingException {
|
||||
final String url = baseUrl + PeertubeSearchQueryHandlerFactory.SEARCH_ENDPOINT;
|
||||
final String url = baseUrl + PeertubeSearchQueryHandlerFactory.SEARCH_ENDPOINT_VIDEOS;
|
||||
final StringBuilder params = new StringBuilder();
|
||||
params.append("start=0&count=8&sort=-createdAt");
|
||||
for (final String tag : tags) {
|
||||
|
@ -420,8 +428,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
// Ignore all exceptions
|
||||
} catch (final Exception e) {
|
||||
subtitlesException = new ParsingException("Could not get subtitles", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ public final class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFac
|
|||
private static final PeertubePlaylistLinkHandlerFactory INSTANCE
|
||||
= new PeertubePlaylistLinkHandlerFactory();
|
||||
private static final String ID_PATTERN = "(/videos/watch/playlist/|/w/p/)([^/?&#]*)";
|
||||
private static final String API_ID_PATTERN = "/video-playlists/([^/?&#]*)";
|
||||
|
||||
private PeertubePlaylistLinkHandlerFactory() {
|
||||
}
|
||||
|
@ -38,7 +39,12 @@ public final class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFac
|
|||
|
||||
@Override
|
||||
public String getId(final String url) throws ParsingException {
|
||||
return Parser.matchGroup(ID_PATTERN, url, 2);
|
||||
try {
|
||||
return Parser.matchGroup(ID_PATTERN, url, 2);
|
||||
} catch (final ParsingException ignored) {
|
||||
// might also be an API url, no reason to throw an exception here
|
||||
}
|
||||
return Parser.matchGroup1(API_ID_PATTERN, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,8 +12,12 @@ public final class PeertubeSearchQueryHandlerFactory extends SearchQueryHandlerF
|
|||
|
||||
public static final String VIDEOS = "videos";
|
||||
public static final String SEPIA_VIDEOS = "sepia_videos"; // sepia is the global index
|
||||
public static final String PLAYLISTS = "playlists";
|
||||
public static final String CHANNELS = "channels";
|
||||
public static final String SEPIA_BASE_URL = "https://sepiasearch.org";
|
||||
public static final String SEARCH_ENDPOINT = "/api/v1/search/videos";
|
||||
public static final String SEARCH_ENDPOINT_PLAYLISTS = "/api/v1/search/video-playlists";
|
||||
public static final String SEARCH_ENDPOINT_VIDEOS = "/api/v1/search/videos";
|
||||
public static final String SEARCH_ENDPOINT_CHANNELS = "/api/v1/search/video-channels";
|
||||
|
||||
private PeertubeSearchQueryHandlerFactory() {
|
||||
}
|
||||
|
@ -41,7 +45,17 @@ public final class PeertubeSearchQueryHandlerFactory extends SearchQueryHandlerF
|
|||
final String sortFilter,
|
||||
final String baseUrl) throws ParsingException {
|
||||
try {
|
||||
return baseUrl + SEARCH_ENDPOINT + "?search=" + Utils.encodeUrlUtf8(searchString);
|
||||
final String endpoint;
|
||||
if (contentFilters.isEmpty()
|
||||
|| contentFilters.get(0).equals(VIDEOS)
|
||||
|| contentFilters.get(0).equals(SEPIA_VIDEOS)) {
|
||||
endpoint = SEARCH_ENDPOINT_VIDEOS;
|
||||
} else if (contentFilters.get(0).equals(CHANNELS)) {
|
||||
endpoint = SEARCH_ENDPOINT_CHANNELS;
|
||||
} else {
|
||||
endpoint = SEARCH_ENDPOINT_PLAYLISTS;
|
||||
}
|
||||
return baseUrl + endpoint + "?search=" + Utils.encodeUrlUtf8(searchString);
|
||||
} catch (final UnsupportedEncodingException e) {
|
||||
throw new ParsingException("Could not encode query", e);
|
||||
}
|
||||
|
@ -51,7 +65,9 @@ public final class PeertubeSearchQueryHandlerFactory extends SearchQueryHandlerF
|
|||
public String[] getAvailableContentFilter() {
|
||||
return new String[]{
|
||||
VIDEOS,
|
||||
SEPIA_VIDEOS
|
||||
PLAYLISTS,
|
||||
CHANNELS,
|
||||
SEPIA_VIDEOS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import org.schabi.newpipe.extractor.ServiceList;
|
|||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -14,27 +12,16 @@ public final class PeertubeTrendingLinkHandlerFactory extends ListLinkHandlerFac
|
|||
private static final PeertubeTrendingLinkHandlerFactory INSTANCE
|
||||
= new PeertubeTrendingLinkHandlerFactory();
|
||||
|
||||
public static final Map<String, String> KIOSK_MAP;
|
||||
public static final Map<String, String> REVERSE_KIOSK_MAP;
|
||||
public static final String KIOSK_TRENDING = "Trending";
|
||||
public static final String KIOSK_MOST_LIKED = "Most liked";
|
||||
public static final String KIOSK_RECENT = "Recently added";
|
||||
public static final String KIOSK_LOCAL = "Local";
|
||||
|
||||
static {
|
||||
final Map<String, String> map = new HashMap<>();
|
||||
map.put(KIOSK_TRENDING, "%s/api/v1/videos?sort=-trending");
|
||||
map.put(KIOSK_MOST_LIKED, "%s/api/v1/videos?sort=-likes");
|
||||
map.put(KIOSK_RECENT, "%s/api/v1/videos?sort=-publishedAt");
|
||||
map.put(KIOSK_LOCAL, "%s/api/v1/videos?sort=-publishedAt&filter=local");
|
||||
KIOSK_MAP = Collections.unmodifiableMap(map);
|
||||
|
||||
final Map<String, String> reverseMap = new HashMap<>();
|
||||
for (final Map.Entry<String, String> entry : KIOSK_MAP.entrySet()) {
|
||||
reverseMap.put(entry.getValue(), entry.getKey());
|
||||
}
|
||||
REVERSE_KIOSK_MAP = Collections.unmodifiableMap(reverseMap);
|
||||
}
|
||||
public static final Map<String, String> KIOSK_MAP = Map.of(
|
||||
KIOSK_TRENDING, "%s/api/v1/videos?sort=-trending",
|
||||
KIOSK_MOST_LIKED, "%s/api/v1/videos?sort=-likes",
|
||||
KIOSK_RECENT, "%s/api/v1/videos?sort=-publishedAt",
|
||||
KIOSK_LOCAL, "%s/api/v1/videos?sort=-publishedAt&filter=local");
|
||||
|
||||
public static PeertubeTrendingLinkHandlerFactory getInstance() {
|
||||
return INSTANCE;
|
||||
|
@ -66,10 +53,12 @@ public final class PeertubeTrendingLinkHandlerFactory extends ListLinkHandlerFac
|
|||
return KIOSK_RECENT;
|
||||
} else if (cleanUrl.contains("/videos/local")) {
|
||||
return KIOSK_LOCAL;
|
||||
} else if (REVERSE_KIOSK_MAP.containsKey(cleanUrl)) {
|
||||
return REVERSE_KIOSK_MAP.get(cleanUrl);
|
||||
} else {
|
||||
throw new ParsingException("no id found for this url");
|
||||
return KIOSK_MAP.entrySet().stream()
|
||||
.filter(entry -> cleanUrl.equals(entry.getValue()))
|
||||
.findFirst()
|
||||
.map(Map.Entry::getKey)
|
||||
.orElseThrow(() -> new ParsingException("no id found for this url"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,8 +64,7 @@ public final class SoundcloudParsingHelper {
|
|||
// The one containing the client id will likely be the last one
|
||||
Collections.reverse(possibleScripts);
|
||||
|
||||
final Map<String, List<String>> headers = Collections.singletonMap("Range",
|
||||
Collections.singletonList("bytes=0-50000"));
|
||||
final var headers = Map.of("Range", List.of("bytes=0-50000"));
|
||||
|
||||
for (final Element element : possibleScripts) {
|
||||
final String srcUrl = element.attr("src");
|
||||
|
|
|
@ -83,12 +83,12 @@ public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtr
|
|||
|
||||
@Override
|
||||
public String getUploaderUrl() {
|
||||
return null;
|
||||
return itemObject.getObject(USER_KEY).getString("permalink_url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() {
|
||||
return false;
|
||||
return itemObject.getObject(USER_KEY).getBoolean("verified");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -133,7 +133,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
|
||||
@Override
|
||||
public long getLikeCount() {
|
||||
return track.getLong("favoritings_count", -1);
|
||||
return track.getLong("likes_count", -1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Serializable;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.schabi.newpipe.extractor.MediaFormat.M4A;
|
||||
import static org.schabi.newpipe.extractor.MediaFormat.MPEG_4;
|
||||
|
@ -198,6 +199,10 @@ public class ItagItem implements Serializable {
|
|||
this.targetDurationSec = itagItem.targetDurationSec;
|
||||
this.approxDurationMs = itagItem.approxDurationMs;
|
||||
this.contentLength = itagItem.contentLength;
|
||||
this.audioTrackId = itagItem.audioTrackId;
|
||||
this.audioTrackName = itagItem.audioTrackName;
|
||||
this.isDescriptiveAudio = itagItem.isDescriptiveAudio;
|
||||
this.audioLocale = itagItem.audioLocale;
|
||||
}
|
||||
|
||||
public MediaFormat getMediaFormat() {
|
||||
|
@ -246,6 +251,9 @@ public class ItagItem implements Serializable {
|
|||
private long contentLength = CONTENT_LENGTH_UNKNOWN;
|
||||
private String audioTrackId;
|
||||
private String audioTrackName;
|
||||
private boolean isDescriptiveAudio;
|
||||
@Nullable
|
||||
private Locale audioLocale;
|
||||
|
||||
public int getBitrate() {
|
||||
return bitrate;
|
||||
|
@ -569,7 +577,7 @@ public class ItagItem implements Serializable {
|
|||
/**
|
||||
* Get the {@code audioTrackName} of the stream, if present.
|
||||
*
|
||||
* @return the {@code audioTrackName} of the stream or null
|
||||
* @return the {@code audioTrackName} of the stream or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public String getAudioTrackName() {
|
||||
|
@ -577,11 +585,53 @@ public class ItagItem implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the {@code audioTrackName} of the stream.
|
||||
* Set the {@code audioTrackName} of the stream, if present.
|
||||
*
|
||||
* @param audioTrackName the {@code audioTrackName} of the stream
|
||||
* @param audioTrackName the {@code audioTrackName} of the stream or {@code null}
|
||||
*/
|
||||
public void setAudioTrackName(@Nullable final String audioTrackName) {
|
||||
this.audioTrackName = audioTrackName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the stream is a descriptive audio.
|
||||
*
|
||||
* @return whether the stream is a descriptive audio
|
||||
*/
|
||||
public boolean isDescriptiveAudio() {
|
||||
return isDescriptiveAudio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the stream is a descriptive audio.
|
||||
*
|
||||
* @param isDescriptiveAudio whether the stream is a descriptive audio
|
||||
*/
|
||||
public void setIsDescriptiveAudio(final boolean isDescriptiveAudio) {
|
||||
this.isDescriptiveAudio = isDescriptiveAudio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the audio {@link Locale} of the stream, if known.
|
||||
*
|
||||
* @return the audio {@link Locale} of the stream, if known, or {@code null} if that's not the
|
||||
* case
|
||||
*/
|
||||
@Nullable
|
||||
public Locale getAudioLocale() {
|
||||
return audioLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio {@link Locale} of the stream.
|
||||
*
|
||||
* <p>
|
||||
* If it is unknown, {@code null} could be passed, which is the default value.
|
||||
* </p>
|
||||
*
|
||||
* @param audioLocale the audio {@link Locale} of the stream, which could be {@code null}
|
||||
*/
|
||||
public void setAudioLocale(@Nullable final Locale audioLocale) {
|
||||
this.audioLocale = audioLocale;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.HTTP;
|
|||
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.getStringResultFromRegexArray;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonBuilder;
|
||||
|
@ -33,6 +32,7 @@ import com.grack.nanojson.JsonObject;
|
|||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
import org.jsoup.nodes.Entities;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException;
|
||||
|
@ -49,8 +49,6 @@ import org.schabi.newpipe.extractor.utils.Parser;
|
|||
import org.schabi.newpipe.extractor.utils.RandomStringFromAlphabetGenerator;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.MalformedURLException;
|
||||
|
@ -62,7 +60,6 @@ import java.time.OffsetDateTime;
|
|||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -74,6 +71,9 @@ import java.util.Set;
|
|||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class YoutubeParsingHelper {
|
||||
|
||||
private YoutubeParsingHelper() {
|
||||
|
@ -90,6 +90,11 @@ public final class YoutubeParsingHelper {
|
|||
public static final String YOUTUBEI_V1_GAPIS_URL =
|
||||
"https://youtubei.googleapis.com/youtubei/v1/";
|
||||
|
||||
/**
|
||||
* The base URL of YouTube Music.
|
||||
*/
|
||||
private static final String YOUTUBE_MUSIC_URL = "https://music.youtube.com";
|
||||
|
||||
/**
|
||||
* A parameter to disable pretty-printed response of InnerTube requests, to reduce response
|
||||
* sizes.
|
||||
|
@ -389,8 +394,7 @@ public final class YoutubeParsingHelper {
|
|||
* @return Whether given id belongs to a YouTube Mix
|
||||
*/
|
||||
public static boolean isYoutubeMixId(@Nonnull final String playlistId) {
|
||||
return playlistId.startsWith("RD")
|
||||
&& !isYoutubeMusicMixId(playlistId);
|
||||
return playlistId.startsWith("RD");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -558,15 +562,13 @@ public final class YoutubeParsingHelper {
|
|||
.end().done().getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("X-YouTube-Client-Name", singletonList("1"));
|
||||
headers.put("X-YouTube-Client-Version",
|
||||
singletonList(HARDCODED_CLIENT_VERSION));
|
||||
final var headers = getClientHeaders("1", HARDCODED_CLIENT_VERSION);
|
||||
|
||||
// This endpoint is fetched by the YouTube website to get the items of its main menu and is
|
||||
// pretty lightweight (around 30kB)
|
||||
final Response response = getDownloader().post(YOUTUBEI_V1_URL + "guide?key="
|
||||
+ HARDCODED_KEY + DISABLE_PRETTY_PRINT_PARAMETER, headers, body);
|
||||
final Response response = getDownloader().postWithContentTypeJson(
|
||||
YOUTUBEI_V1_URL + "guide?key=" + HARDCODED_KEY + DISABLE_PRETTY_PRINT_PARAMETER,
|
||||
headers, body);
|
||||
final String responseBody = response.responseBody();
|
||||
final int responseCode = response.responseCode();
|
||||
|
||||
|
@ -582,9 +584,7 @@ public final class YoutubeParsingHelper {
|
|||
return;
|
||||
}
|
||||
final String url = "https://www.youtube.com/sw.js";
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("Origin", singletonList("https://www.youtube.com"));
|
||||
headers.put("Referer", singletonList("https://www.youtube.com"));
|
||||
final var headers = getOriginReferrerHeaders("https://www.youtube.com");
|
||||
final String response = getDownloader().get(url, headers).responseBody();
|
||||
try {
|
||||
clientVersion = getStringResultFromRegexArray(response,
|
||||
|
@ -803,16 +803,11 @@ public final class YoutubeParsingHelper {
|
|||
.end().done().getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("X-YouTube-Client-Name", singletonList(
|
||||
HARDCODED_YOUTUBE_MUSIC_KEY[1]));
|
||||
headers.put("X-YouTube-Client-Version", singletonList(
|
||||
final var headers = new HashMap<>(getOriginReferrerHeaders(YOUTUBE_MUSIC_URL));
|
||||
headers.putAll(getClientHeaders(HARDCODED_YOUTUBE_MUSIC_KEY[1],
|
||||
HARDCODED_YOUTUBE_MUSIC_KEY[2]));
|
||||
headers.put("Origin", singletonList("https://music.youtube.com"));
|
||||
headers.put("Referer", singletonList("music.youtube.com"));
|
||||
headers.put("Content-Type", singletonList("application/json"));
|
||||
|
||||
final Response response = getDownloader().post(url, headers, json);
|
||||
final Response response = getDownloader().postWithContentTypeJson(url, headers, json);
|
||||
// Ensure to have a valid response
|
||||
return response.responseBody().length() > 500 && response.responseCode() == 200;
|
||||
}
|
||||
|
@ -833,14 +828,12 @@ public final class YoutubeParsingHelper {
|
|||
|
||||
try {
|
||||
final String url = "https://music.youtube.com/sw.js";
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("Origin", singletonList("https://music.youtube.com"));
|
||||
headers.put("Referer", singletonList("https://music.youtube.com"));
|
||||
final var headers = getOriginReferrerHeaders(YOUTUBE_MUSIC_URL);
|
||||
final String response = getDownloader().get(url, headers).responseBody();
|
||||
musicClientVersion = getStringResultFromRegexArray(response,
|
||||
INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES, 1);
|
||||
musicKey = getStringResultFromRegexArray(response, INNERTUBE_API_KEY_REGEXES, 1);
|
||||
musicClientName = Parser.matchGroup1(INNERTUBE_CLIENT_NAME_REGEX, response);
|
||||
musicClientVersion = getStringResultFromRegexArray(response,
|
||||
INNERTUBE_CONTEXT_CLIENT_VERSION_REGEXES, 1);
|
||||
musicKey = getStringResultFromRegexArray(response, INNERTUBE_API_KEY_REGEXES, 1);
|
||||
musicClientName = Parser.matchGroup1(INNERTUBE_CLIENT_NAME_REGEX, response);
|
||||
} catch (final Exception e) {
|
||||
final String url = "https://music.youtube.com/?ucbcb=1";
|
||||
final String html = getDownloader().get(url, getCookieHeader()).responseBody();
|
||||
|
@ -856,10 +849,11 @@ public final class YoutubeParsingHelper {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public static String getUrlFromNavigationEndpoint(@Nonnull final JsonObject navigationEndpoint)
|
||||
throws ParsingException {
|
||||
public static String getUrlFromNavigationEndpoint(
|
||||
@Nonnull final JsonObject navigationEndpoint) {
|
||||
if (navigationEndpoint.has("urlEndpoint")) {
|
||||
String internUrl = navigationEndpoint.getObject("urlEndpoint").getString("url");
|
||||
String internUrl = navigationEndpoint.getObject("urlEndpoint")
|
||||
.getString("url");
|
||||
if (internUrl.startsWith("https://www.youtube.com/redirect?")) {
|
||||
// remove https://www.youtube.com part to fall in the next if block
|
||||
internUrl = internUrl.substring(23);
|
||||
|
@ -884,7 +878,9 @@ public final class YoutubeParsingHelper {
|
|||
|| internUrl.startsWith("/watch")) {
|
||||
return "https://www.youtube.com" + internUrl;
|
||||
}
|
||||
} else if (navigationEndpoint.has("browseEndpoint")) {
|
||||
}
|
||||
|
||||
if (navigationEndpoint.has("browseEndpoint")) {
|
||||
final JsonObject browseEndpoint = navigationEndpoint.getObject("browseEndpoint");
|
||||
final String canonicalBaseUrl = browseEndpoint.getString("canonicalBaseUrl");
|
||||
final String browseId = browseEndpoint.getString("browseId");
|
||||
|
@ -897,26 +893,39 @@ public final class YoutubeParsingHelper {
|
|||
if (!isNullOrEmpty(canonicalBaseUrl)) {
|
||||
return "https://www.youtube.com" + canonicalBaseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ParsingException("canonicalBaseUrl is null and browseId is not a channel (\""
|
||||
+ browseEndpoint + "\")");
|
||||
} else if (navigationEndpoint.has("watchEndpoint")) {
|
||||
if (navigationEndpoint.has("watchEndpoint")) {
|
||||
final StringBuilder url = new StringBuilder();
|
||||
url.append("https://www.youtube.com/watch?v=").append(navigationEndpoint
|
||||
.getObject("watchEndpoint").getString(VIDEO_ID));
|
||||
url.append("https://www.youtube.com/watch?v=")
|
||||
.append(navigationEndpoint.getObject("watchEndpoint")
|
||||
.getString(VIDEO_ID));
|
||||
if (navigationEndpoint.getObject("watchEndpoint").has("playlistId")) {
|
||||
url.append("&list=").append(navigationEndpoint.getObject("watchEndpoint")
|
||||
.getString("playlistId"));
|
||||
}
|
||||
if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds")) {
|
||||
url.append("&t=").append(navigationEndpoint.getObject("watchEndpoint")
|
||||
url.append("&t=")
|
||||
.append(navigationEndpoint.getObject("watchEndpoint")
|
||||
.getInt("startTimeSeconds"));
|
||||
}
|
||||
return url.toString();
|
||||
} else if (navigationEndpoint.has("watchPlaylistEndpoint")) {
|
||||
return "https://www.youtube.com/playlist?list="
|
||||
+ navigationEndpoint.getObject("watchPlaylistEndpoint").getString("playlistId");
|
||||
}
|
||||
|
||||
if (navigationEndpoint.has("watchPlaylistEndpoint")) {
|
||||
return "https://www.youtube.com/playlist?list="
|
||||
+ navigationEndpoint.getObject("watchPlaylistEndpoint")
|
||||
.getString("playlistId");
|
||||
}
|
||||
|
||||
if (navigationEndpoint.has("commandMetadata")) {
|
||||
final JsonObject metadata = navigationEndpoint.getObject("commandMetadata")
|
||||
.getObject("webCommandMetadata");
|
||||
if (metadata.has("url")) {
|
||||
return "https://www.youtube.com" + metadata.getString("url");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -929,8 +938,7 @@ public final class YoutubeParsingHelper {
|
|||
* @return text in the JSON object or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static String getTextFromObject(final JsonObject textObject, final boolean html)
|
||||
throws ParsingException {
|
||||
public static String getTextFromObject(final JsonObject textObject, final boolean html) {
|
||||
if (isNullOrEmpty(textObject)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -950,10 +958,11 @@ public final class YoutubeParsingHelper {
|
|||
|
||||
if (html) {
|
||||
if (run.has("navigationEndpoint")) {
|
||||
final String url = getUrlFromNavigationEndpoint(run
|
||||
.getObject("navigationEndpoint"));
|
||||
final String url = getUrlFromNavigationEndpoint(
|
||||
run.getObject("navigationEndpoint"));
|
||||
if (!isNullOrEmpty(url)) {
|
||||
text = "<a href=\"" + url + "\">" + text + "</a>";
|
||||
text = "<a href=\"" + Entities.escape(url) + "\">" + Entities.escape(text)
|
||||
+ "</a>";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1019,11 +1028,12 @@ public final class YoutubeParsingHelper {
|
|||
}
|
||||
|
||||
final String content = attributedDescription.getString("content");
|
||||
final JsonArray commandRuns = attributedDescription.getArray("commandRuns");
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final JsonArray commandRuns = attributedDescription.getArray("commandRuns");
|
||||
|
||||
final StringBuilder textBuilder = new StringBuilder();
|
||||
int textStart = 0;
|
||||
|
||||
|
@ -1042,12 +1052,7 @@ public final class YoutubeParsingHelper {
|
|||
continue;
|
||||
}
|
||||
|
||||
final String url;
|
||||
try {
|
||||
url = getUrlFromNavigationEndpoint(navigationEndpoint);
|
||||
} catch (final ParsingException e) {
|
||||
continue;
|
||||
}
|
||||
final String url = getUrlFromNavigationEndpoint(navigationEndpoint);
|
||||
|
||||
if (url == null) {
|
||||
continue;
|
||||
|
@ -1066,9 +1071,9 @@ public final class YoutubeParsingHelper {
|
|||
.replaceFirst("^[/•] *", "");
|
||||
|
||||
textBuilder.append("<a href=\"")
|
||||
.append(url)
|
||||
.append(Entities.escape(url))
|
||||
.append("\">")
|
||||
.append(linkText)
|
||||
.append(Entities.escape(linkText))
|
||||
.append("</a>");
|
||||
|
||||
textStart = startIndex + length;
|
||||
|
@ -1085,13 +1090,12 @@ public final class YoutubeParsingHelper {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public static String getTextFromObject(final JsonObject textObject) throws ParsingException {
|
||||
public static String getTextFromObject(final JsonObject textObject) {
|
||||
return getTextFromObject(textObject, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getUrlFromObject(final JsonObject textObject) throws ParsingException {
|
||||
|
||||
public static String getUrlFromObject(final JsonObject textObject) {
|
||||
if (isNullOrEmpty(textObject)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1112,8 +1116,7 @@ public final class YoutubeParsingHelper {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public static String getTextAtKey(@Nonnull final JsonObject jsonObject, final String theKey)
|
||||
throws ParsingException {
|
||||
public static String getTextAtKey(@Nonnull final JsonObject jsonObject, final String theKey) {
|
||||
if (jsonObject.isString(theKey)) {
|
||||
return jsonObject.getString(theKey);
|
||||
} else {
|
||||
|
@ -1183,14 +1186,11 @@ public final class YoutubeParsingHelper {
|
|||
final byte[] body,
|
||||
final Localization localization)
|
||||
throws IOException, ExtractionException {
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
addClientInfoHeaders(headers);
|
||||
headers.put("Content-Type", singletonList("application/json"));
|
||||
final var headers = getYouTubeHeaders();
|
||||
|
||||
final Response response = getDownloader().post(YOUTUBEI_V1_URL + endpoint + "?key="
|
||||
+ getKey() + DISABLE_PRETTY_PRINT_PARAMETER, headers, body, localization);
|
||||
|
||||
return JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
return JsonUtils.toJsonObject(getValidJsonResponseBody(
|
||||
getDownloader().postWithContentTypeJson(YOUTUBEI_V1_URL + endpoint + "?key="
|
||||
+ getKey() + DISABLE_PRETTY_PRINT_PARAMETER, headers, body, localization)));
|
||||
}
|
||||
|
||||
public static JsonObject getJsonAndroidPostResponse(
|
||||
|
@ -1218,18 +1218,17 @@ public final class YoutubeParsingHelper {
|
|||
@Nonnull final String userAgent,
|
||||
@Nonnull final String innerTubeApiKey,
|
||||
@Nullable final String endPartOfUrlRequest) throws IOException, ExtractionException {
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("Content-Type", singletonList("application/json"));
|
||||
headers.put("User-Agent", singletonList(userAgent));
|
||||
headers.put("X-Goog-Api-Format-Version", singletonList("2"));
|
||||
final var headers = Map.of("User-Agent", List.of(userAgent),
|
||||
"X-Goog-Api-Format-Version", List.of("2"));
|
||||
|
||||
final String baseEndpointUrl = YOUTUBEI_V1_GAPIS_URL + endpoint + "?key=" + innerTubeApiKey
|
||||
+ DISABLE_PRETTY_PRINT_PARAMETER;
|
||||
|
||||
final Response response = getDownloader().post(isNullOrEmpty(endPartOfUrlRequest)
|
||||
? baseEndpointUrl : baseEndpointUrl + endPartOfUrlRequest,
|
||||
headers, body, localization);
|
||||
return JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
return JsonUtils.toJsonObject(getValidJsonResponseBody(
|
||||
getDownloader().postWithContentTypeJson(isNullOrEmpty(endPartOfUrlRequest)
|
||||
? baseEndpointUrl
|
||||
: baseEndpointUrl + endPartOfUrlRequest,
|
||||
headers, body, localization)));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
@ -1451,29 +1450,59 @@ public final class YoutubeParsingHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add required headers and cookies to an existing headers Map.
|
||||
* @see #addClientInfoHeaders(Map)
|
||||
* @see #addCookieHeader(Map)
|
||||
* Returns a {@link Map} containing the required YouTube Music headers.
|
||||
*/
|
||||
public static void addYouTubeHeaders(final Map<String, List<String>> headers)
|
||||
throws IOException, ExtractionException {
|
||||
addClientInfoHeaders(headers);
|
||||
addCookieHeader(headers);
|
||||
@Nonnull
|
||||
public static Map<String, List<String>> getYoutubeMusicHeaders() {
|
||||
final var headers = new HashMap<>(getOriginReferrerHeaders(YOUTUBE_MUSIC_URL));
|
||||
headers.putAll(getClientHeaders(youtubeMusicKey[1], youtubeMusicKey[2]));
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the <code>X-YouTube-Client-Name</code>, <code>X-YouTube-Client-Version</code>,
|
||||
* <code>Origin</code>, and <code>Referer</code> headers.
|
||||
* @param headers The headers which should be completed
|
||||
* Returns a {@link Map} containing the required YouTube headers, including the
|
||||
* <code>CONSENT</code> cookie to prevent redirects to <code>consent.youtube.com</code>
|
||||
*/
|
||||
public static void addClientInfoHeaders(@Nonnull final Map<String, List<String>> headers)
|
||||
throws IOException, ExtractionException {
|
||||
headers.computeIfAbsent("Origin", k -> singletonList("https://www.youtube.com"));
|
||||
headers.computeIfAbsent("Referer", k -> singletonList("https://www.youtube.com"));
|
||||
headers.computeIfAbsent("X-YouTube-Client-Name", k -> singletonList("1"));
|
||||
if (headers.get("X-YouTube-Client-Version") == null) {
|
||||
headers.put("X-YouTube-Client-Version", singletonList(getClientVersion()));
|
||||
}
|
||||
public static Map<String, List<String>> getYouTubeHeaders()
|
||||
throws ExtractionException, IOException {
|
||||
final var headers = getClientInfoHeaders();
|
||||
headers.put("Cookie", List.of(generateConsentCookie()));
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Map} containing the {@code X-YouTube-Client-Name},
|
||||
* {@code X-YouTube-Client-Version}, {@code Origin}, and {@code Referer} headers.
|
||||
*/
|
||||
public static Map<String, List<String>> getClientInfoHeaders()
|
||||
throws ExtractionException, IOException {
|
||||
final var headers = new HashMap<>(getOriginReferrerHeaders("https://www.youtube.com"));
|
||||
headers.putAll(getClientHeaders("1", getClientVersion()));
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable {@link Map} containing the {@code Origin} and {@code Referer}
|
||||
* headers set to the given URL.
|
||||
*
|
||||
* @param url The URL to be set as the origin and referrer.
|
||||
*/
|
||||
private static Map<String, List<String>> getOriginReferrerHeaders(@Nonnull final String url) {
|
||||
final var urlList = List.of(url);
|
||||
return Map.of("Origin", urlList, "Referer", urlList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable {@link Map} containing the {@code X-YouTube-Client-Name} and
|
||||
* {@code X-YouTube-Client-Version} headers.
|
||||
*
|
||||
* @param name The X-YouTube-Client-Name value.
|
||||
* @param version X-YouTube-Client-Version value.
|
||||
*/
|
||||
private static Map<String, List<String>> getClientHeaders(@Nonnull final String name,
|
||||
@Nonnull final String version) {
|
||||
return Map.of("X-YouTube-Client-Name", List.of(name),
|
||||
"X-YouTube-Client-Version", List.of(version));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1481,19 +1510,7 @@ public final class YoutubeParsingHelper {
|
|||
* @return A singleton map containing the header.
|
||||
*/
|
||||
public static Map<String, List<String>> getCookieHeader() {
|
||||
return Collections.singletonMap("Cookie", singletonList(generateConsentCookie()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the <code>CONSENT</code> cookie to prevent redirect to <code>consent.youtube.com</code>
|
||||
* @param headers the headers which should be completed
|
||||
*/
|
||||
public static void addCookieHeader(@Nonnull final Map<String, List<String>> headers) {
|
||||
if (headers.get("Cookie") == null) {
|
||||
headers.put("Cookie", Collections.singletonList(generateConsentCookie()));
|
||||
} else {
|
||||
headers.get("Cookie").add(generateConsentCookie());
|
||||
}
|
||||
return Map.of("Cookie", List.of(generateConsentCookie()));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -122,8 +122,7 @@ public class YoutubeService extends StreamingService {
|
|||
|
||||
@Override
|
||||
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
|
||||
if (YoutubeParsingHelper.isYoutubeMixId(linkHandler.getId())
|
||||
&& !YoutubeParsingHelper.isYoutubeMusicMixId(linkHandler.getId())) {
|
||||
if (YoutubeParsingHelper.isYoutubeMixId(linkHandler.getId())) {
|
||||
return new YoutubeMixPlaylistExtractor(this, linkHandler);
|
||||
} else {
|
||||
return new YoutubePlaylistExtractor(this, linkHandler);
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientInfoHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
|
@ -13,6 +22,14 @@ import org.w3c.dom.DOMException;
|
|||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
|
@ -25,25 +42,6 @@ import javax.xml.transform.TransformerFactory;
|
|||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
/**
|
||||
* Utilities and constants for YouTube DASH manifest creators.
|
||||
*
|
||||
|
@ -126,7 +124,7 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
* <li>{@code Period} (using {@link #generatePeriodElement(Document)});</li>
|
||||
* <li>{@code AdaptationSet} (using {@link #generateAdaptationSetElement(Document,
|
||||
* ItagItem)});</li>
|
||||
* <li>{@code Role} (using {@link #generateRoleElement(Document)});</li>
|
||||
* <li>{@code Role} (using {@link #generateRoleElement(Document, ItagItem)});</li>
|
||||
* <li>{@code Representation} (using {@link #generateRepresentationElement(Document,
|
||||
* ItagItem)});</li>
|
||||
* <li>and, for audio streams, {@code AudioChannelConfiguration} (using
|
||||
|
@ -146,7 +144,7 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
|
||||
generatePeriodElement(doc);
|
||||
generateAdaptationSetElement(doc, itagItem);
|
||||
generateRoleElement(doc);
|
||||
generateRoleElement(doc, itagItem);
|
||||
generateRepresentationElement(doc, itagItem);
|
||||
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
|
||||
generateAudioChannelConfigurationElement(doc, itagItem);
|
||||
|
@ -210,7 +208,7 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
* {@link #generateDocumentAndMpdElement(long)}.
|
||||
* </p>
|
||||
*
|
||||
* @param doc the {@link Document} on which the the {@code <Period>} element will be appended
|
||||
* @param doc the {@link Document} on which the {@code <Period>} element will be appended
|
||||
*/
|
||||
public static void generatePeriodElement(@Nonnull final Document doc)
|
||||
throws CreationException {
|
||||
|
@ -251,6 +249,16 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
"the MediaFormat or its mime type is null or empty");
|
||||
}
|
||||
|
||||
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
|
||||
final Locale audioLocale = itagItem.getAudioLocale();
|
||||
if (audioLocale != null) {
|
||||
final String audioLanguage = audioLocale.getLanguage();
|
||||
if (!audioLanguage.isEmpty()) {
|
||||
setAttribute(adaptationSetElement, doc, "lang", audioLanguage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAttribute(adaptationSetElement, doc, "mimeType", mediaFormat.getMimeType());
|
||||
setAttribute(adaptationSetElement, doc, "subsegmentAlignment", "true");
|
||||
|
||||
|
@ -269,7 +277,8 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* {@code <Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>}
|
||||
* {@code <Role schemeIdUri="urn:mpeg:DASH:role:2011" value="VALUE"/>}, where {@code VALUE} is
|
||||
* {@code main} for videos and audios and {@code alternate} for descriptive audio
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -277,9 +286,11 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
* {@link #generateAdaptationSetElement(Document, ItagItem)}).
|
||||
* </p>
|
||||
*
|
||||
* @param doc the {@link Document} on which the the {@code <Role>} element will be appended
|
||||
* @param doc the {@link Document} on which the {@code <Role>} element will be appended
|
||||
* @param itagItem the {@link ItagItem} corresponding to the stream, which must not be null
|
||||
*/
|
||||
public static void generateRoleElement(@Nonnull final Document doc)
|
||||
public static void generateRoleElement(@Nonnull final Document doc,
|
||||
@Nonnull final ItagItem itagItem)
|
||||
throws CreationException {
|
||||
try {
|
||||
final Element adaptationSetElement = (Element) doc.getElementsByTagName(
|
||||
|
@ -287,7 +298,8 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
final Element roleElement = doc.createElement(ROLE);
|
||||
|
||||
setAttribute(roleElement, doc, "schemeIdUri", "urn:mpeg:DASH:role:2011");
|
||||
setAttribute(roleElement, doc, "value", "main");
|
||||
setAttribute(roleElement, doc, "value", itagItem.isDescriptiveAudio()
|
||||
? "alternate" : "main");
|
||||
|
||||
adaptationSetElement.appendChild(roleElement);
|
||||
} catch (final DOMException e) {
|
||||
|
@ -304,7 +316,7 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
* {@link #generateAdaptationSetElement(Document, ItagItem)}).
|
||||
* </p>
|
||||
*
|
||||
* @param doc the {@link Document} on which the the {@code <SegmentTimeline>} element will be
|
||||
* @param doc the {@link Document} on which the {@code <SegmentTimeline>} element will be
|
||||
* appended
|
||||
* @param itagItem the {@link ItagItem} to use, which must not be null
|
||||
*/
|
||||
|
@ -524,7 +536,7 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
* {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}.
|
||||
* </p>
|
||||
*
|
||||
* @param doc the {@link Document} on which the the {@code <SegmentTimeline>} element will be
|
||||
* @param doc the {@link Document} on which the {@code <SegmentTimeline>} element will be
|
||||
* appended
|
||||
*/
|
||||
public static void generateSegmentTimelineElement(@Nonnull final Document doc)
|
||||
|
@ -584,9 +596,9 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
}
|
||||
} else if (isAndroidStreamingUrl || isIosStreamingUrl) {
|
||||
try {
|
||||
final Map<String, List<String>> headers = Collections.singletonMap("User-Agent",
|
||||
Collections.singletonList(isAndroidStreamingUrl
|
||||
? getAndroidUserAgent(null) : getIosUserAgent(null)));
|
||||
final var headers = Map.of("User-Agent",
|
||||
List.of(isAndroidStreamingUrl ? getAndroidUserAgent(null)
|
||||
: getIosUserAgent(null)));
|
||||
final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8);
|
||||
return downloader.post(baseStreamingUrl, headers, emptyBody);
|
||||
} catch (final IOException | ExtractionException e) {
|
||||
|
@ -706,8 +718,7 @@ public final class YoutubeDashManifestCreatorsUtils {
|
|||
@Nonnull final String responseMimeTypeExpected)
|
||||
throws CreationException {
|
||||
try {
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
addClientInfoHeaders(headers);
|
||||
final var headers = getClientInfoHeaders();
|
||||
|
||||
String responseMimeType = "";
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
|
|||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.ChannelResponseData;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getChannelResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.resolveChannelId;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
@ -20,7 +19,6 @@ import org.schabi.newpipe.extractor.Page;
|
|||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
@ -32,21 +30,19 @@ import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelL
|
|||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelTabLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 25.07.16.
|
||||
*
|
||||
|
@ -90,8 +86,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
||||
ExtractionException {
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final String channelPath = super.getId();
|
||||
final String id = resolveChannelId(channelPath);
|
||||
final ChannelResponseData data = getChannelResponse(id, "EgZ2aWRlb3M%3D",
|
||||
|
@ -272,8 +268,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page) throws IOException,
|
||||
ExtractionException {
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||
}
|
||||
|
@ -281,14 +277,10 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||
final List<String> channelIds = page.getIds();
|
||||
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
addClientInfoHeaders(headers);
|
||||
|
||||
final Response response = getDownloader().post(page.getUrl(), headers, page.getBody(),
|
||||
final JsonObject ajaxJson = getJsonPostResponse("browse", page.getBody(),
|
||||
getExtractorLocalization());
|
||||
|
||||
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
|
||||
final JsonObject sectionListContinuation = ajaxJson.getArray("onResponseReceivedActions")
|
||||
.getObject(0)
|
||||
.getObject("appendContinuationItemsAction");
|
||||
|
@ -301,8 +293,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||
|
||||
@Nullable
|
||||
private Page getNextPageFrom(final JsonObject continuations,
|
||||
final List<String> channelIds) throws IOException,
|
||||
ExtractionException {
|
||||
final List<String> channelIds)
|
||||
throws IOException, ExtractionException {
|
||||
if (isNullOrEmpty(continuations)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -380,8 +372,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||
|
||||
@Nullable
|
||||
private JsonObject getVideoTab() throws ParsingException {
|
||||
if (this.videoTab != null) {
|
||||
return this.videoTab;
|
||||
if (videoTab != null) {
|
||||
return videoTab;
|
||||
}
|
||||
|
||||
final JsonArray responseTabs = initialData.getObject("contents")
|
||||
|
@ -438,19 +430,22 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final String messageRendererText = getTextFromObject(foundVideoTab.getObject("content")
|
||||
.getObject("sectionListRenderer").getArray("contents").getObject(0)
|
||||
.getObject("itemSectionRenderer").getArray("contents").getObject(0)
|
||||
.getObject("messageRenderer").getObject("text"));
|
||||
if (messageRendererText != null
|
||||
&& messageRendererText.equals("This channel has no videos.")) {
|
||||
return null;
|
||||
}
|
||||
} catch (final ParsingException ignored) {
|
||||
final String messageRendererText = getTextFromObject(
|
||||
foundVideoTab.getObject("content")
|
||||
.getObject("sectionListRenderer")
|
||||
.getArray("contents")
|
||||
.getObject(0)
|
||||
.getObject("itemSectionRenderer")
|
||||
.getArray("contents")
|
||||
.getObject(0)
|
||||
.getObject("messageRenderer")
|
||||
.getObject("text"));
|
||||
if (messageRendererText != null
|
||||
&& messageRendererText.equals("This channel has no videos.")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.videoTab = foundVideoTab;
|
||||
videoTab = foundVideoTab;
|
||||
return foundVideoTab;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,23 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
|||
|
||||
public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor {
|
||||
private final JsonObject channelInfoItem;
|
||||
/**
|
||||
* New layout:
|
||||
* "subscriberCountText": Channel handle
|
||||
* "videoCountText": Subscriber count
|
||||
*/
|
||||
private final boolean withHandle;
|
||||
|
||||
public YoutubeChannelInfoItemExtractor(final JsonObject channelInfoItem) {
|
||||
this.channelInfoItem = channelInfoItem;
|
||||
|
||||
boolean wHandle = false;
|
||||
final String subscriberCountText = getTextFromObject(
|
||||
channelInfoItem.getObject("subscriberCountText"));
|
||||
if (subscriberCountText != null) {
|
||||
wHandle = subscriberCountText.startsWith("@");
|
||||
}
|
||||
this.withHandle = wHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -78,6 +92,15 @@ public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (withHandle) {
|
||||
if (channelInfoItem.has("videoCountText")) {
|
||||
return Utils.mixedNumberWordToLong(getTextFromObject(
|
||||
channelInfoItem.getObject("videoCountText")));
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return Utils.mixedNumberWordToLong(getTextFromObject(
|
||||
channelInfoItem.getObject("subscriberCountText")));
|
||||
} catch (final Exception e) {
|
||||
|
@ -88,8 +111,9 @@ public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor
|
|||
@Override
|
||||
public long getStreamCount() throws ParsingException {
|
||||
try {
|
||||
if (!channelInfoItem.has("videoCountText")) {
|
||||
// Video count is not available, channel probably has no public uploads.
|
||||
if (withHandle || !channelInfoItem.has("videoCountText")) {
|
||||
// Video count is not available, either the channel has no public uploads
|
||||
// or YouTube displays the channel handle instead.
|
||||
return ListExtractor.ITEM_COUNT_UNKNOWN;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,13 +9,11 @@ import org.schabi.newpipe.extractor.Page;
|
|||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelTabExtractor;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ChannelTabs;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelTabLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -23,18 +21,15 @@ import java.io.IOException;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.ChannelResponseData;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getChannelResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.resolveChannelId;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
@ -159,14 +154,10 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
|
|||
final List<String> channelIds = page.getIds();
|
||||
|
||||
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
addClientInfoHeaders(headers);
|
||||
|
||||
final Response response = getDownloader().post(page.getUrl(), headers, page.getBody(),
|
||||
final JsonObject ajaxJson = getJsonPostResponse("browse", page.getBody(),
|
||||
getExtractorLocalization());
|
||||
|
||||
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
|
||||
final JsonObject sectionListContinuation = ajaxJson.getArray("onResponseReceivedActions")
|
||||
.getObject(0)
|
||||
.getObject("appendContinuationItemsAction");
|
||||
|
|
|
@ -1,18 +1,8 @@
|
|||
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||
|
@ -24,26 +14,31 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public class YoutubeCommentsExtractor extends CommentsExtractor {
|
||||
|
||||
private JsonObject nextResponse;
|
||||
/**
|
||||
* Whether comments are disabled on video.
|
||||
*/
|
||||
private boolean commentsDisabled;
|
||||
|
||||
/**
|
||||
* Caching mechanism and holder of the commentsDisabled value.
|
||||
* <br/>
|
||||
* Initial value = empty -> unknown if comments are disabled or not<br/>
|
||||
* Some method calls {@link #findInitialCommentsToken()}
|
||||
* -> value is set<br/>
|
||||
* If the method or another one that is depending on disabled comments
|
||||
* is now called again, the method execution can avoid unnecessary calls
|
||||
* The second ajax <b>/next</b> response.
|
||||
*/
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private Optional<Boolean> optCommentsDisabled = Optional.empty();
|
||||
private JsonObject ajaxJson;
|
||||
|
||||
public YoutubeCommentsExtractor(
|
||||
final StreamingService service,
|
||||
|
@ -56,32 +51,25 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
|||
public InfoItemsPage<CommentsInfoItem> getInitialPage()
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
// Check if findInitialCommentsToken was already called and optCommentsDisabled initialized
|
||||
if (optCommentsDisabled.orElse(false)) {
|
||||
if (commentsDisabled) {
|
||||
return getInfoItemsPageForDisabledComments();
|
||||
}
|
||||
|
||||
// Get the token
|
||||
final String commentsToken = findInitialCommentsToken();
|
||||
// Check if the comments have been disabled
|
||||
if (optCommentsDisabled.get()) {
|
||||
return getInfoItemsPageForDisabledComments();
|
||||
}
|
||||
|
||||
return getPage(getNextPage(commentsToken));
|
||||
return extractComments(ajaxJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the initial comments token and initializes commentsDisabled.
|
||||
* <br/>
|
||||
* Also sets {@link #optCommentsDisabled}.
|
||||
* Also sets {@link #commentsDisabled}.
|
||||
*
|
||||
* @return the continuation token or null if none was found
|
||||
*/
|
||||
@Nullable
|
||||
private String findInitialCommentsToken() throws ExtractionException {
|
||||
private String findInitialCommentsToken(final JsonObject nextResponse)
|
||||
throws ExtractionException {
|
||||
final String token = JsonUtils.getArray(nextResponse,
|
||||
"contents.twoColumnWatchNextResults.results.results.contents")
|
||||
"contents.twoColumnWatchNextResults.results.results.contents")
|
||||
.stream()
|
||||
// Only use JsonObjects
|
||||
.filter(JsonObject.class::isInstance)
|
||||
|
@ -112,7 +100,7 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
|||
.orElse(null);
|
||||
|
||||
// The comments are disabled if we couldn't get a token
|
||||
optCommentsDisabled = Optional.of(token == null);
|
||||
commentsDisabled = token == null;
|
||||
|
||||
return token;
|
||||
}
|
||||
|
@ -123,9 +111,9 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private Page getNextPage(@Nonnull final JsonObject ajaxJson) throws ExtractionException {
|
||||
private Page getNextPage(@Nonnull final JsonObject jsonObject) throws ExtractionException {
|
||||
final JsonArray onResponseReceivedEndpoints =
|
||||
ajaxJson.getArray("onResponseReceivedEndpoints");
|
||||
jsonObject.getArray("onResponseReceivedEndpoints");
|
||||
|
||||
// Prevent ArrayIndexOutOfBoundsException
|
||||
if (onResponseReceivedEndpoints.isEmpty()) {
|
||||
|
@ -173,33 +161,43 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
|||
@Override
|
||||
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
if (optCommentsDisabled.orElse(false)) {
|
||||
|
||||
if (commentsDisabled) {
|
||||
return getInfoItemsPageForDisabledComments();
|
||||
}
|
||||
|
||||
if (page == null || isNullOrEmpty(page.getId())) {
|
||||
throw new IllegalArgumentException("Page doesn't have the continuation.");
|
||||
}
|
||||
|
||||
final Localization localization = getExtractorLocalization();
|
||||
// @formatter:off
|
||||
final byte[] body = JsonWriter.string(
|
||||
prepareDesktopJsonBuilder(localization, getExtractorContentCountry())
|
||||
.value("continuation", page.getId())
|
||||
.done())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
final JsonObject ajaxJson = getJsonPostResponse("next", body, localization);
|
||||
final JsonObject jsonObject = getJsonPostResponse("next", body, localization);
|
||||
|
||||
return extractComments(jsonObject);
|
||||
}
|
||||
|
||||
private InfoItemsPage<CommentsInfoItem> extractComments(final JsonObject jsonObject)
|
||||
throws ExtractionException {
|
||||
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(
|
||||
getServiceId());
|
||||
collectCommentsFrom(collector, ajaxJson);
|
||||
return new InfoItemsPage<>(collector, getNextPage(ajaxJson));
|
||||
collectCommentsFrom(collector, jsonObject);
|
||||
return new InfoItemsPage<>(collector, getNextPage(jsonObject));
|
||||
}
|
||||
|
||||
private void collectCommentsFrom(final CommentsInfoItemsCollector collector,
|
||||
@Nonnull final JsonObject ajaxJson) throws ParsingException {
|
||||
final JsonObject jsonObject)
|
||||
throws ParsingException {
|
||||
|
||||
final JsonArray onResponseReceivedEndpoints =
|
||||
ajaxJson.getArray("onResponseReceivedEndpoints");
|
||||
jsonObject.getArray("onResponseReceivedEndpoints");
|
||||
// Prevent ArrayIndexOutOfBoundsException
|
||||
if (onResponseReceivedEndpoints.isEmpty()) {
|
||||
return;
|
||||
|
@ -254,24 +252,59 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
|||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final Localization localization = getExtractorLocalization();
|
||||
// @formatter:off
|
||||
final byte[] body = JsonWriter.string(
|
||||
prepareDesktopJsonBuilder(localization, getExtractorContentCountry())
|
||||
.value("videoId", getId())
|
||||
.done())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
nextResponse = getJsonPostResponse("next", body, localization);
|
||||
final String initialToken =
|
||||
findInitialCommentsToken(getJsonPostResponse("next", body, localization));
|
||||
|
||||
if (initialToken == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
final byte[] ajaxBody = JsonWriter.string(
|
||||
prepareDesktopJsonBuilder(localization, getExtractorContentCountry())
|
||||
.value("continuation", initialToken)
|
||||
.done())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
ajaxJson = getJsonPostResponse("next", ajaxBody, localization);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isCommentsDisabled() throws ExtractionException {
|
||||
// Check if commentsDisabled has to be initialized
|
||||
if (!optCommentsDisabled.isPresent()) {
|
||||
// Initialize commentsDisabled
|
||||
this.findInitialCommentsToken();
|
||||
public boolean isCommentsDisabled() {
|
||||
return commentsDisabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCommentsCount() throws ExtractionException {
|
||||
assertPageFetched();
|
||||
|
||||
if (commentsDisabled) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return optCommentsDisabled.get();
|
||||
final JsonObject countText = ajaxJson
|
||||
.getArray("onResponseReceivedEndpoints").getObject(0)
|
||||
.getObject("reloadContinuationItemsCommand")
|
||||
.getArray("continuationItems").getObject(0)
|
||||
.getObject("commentsHeaderRenderer")
|
||||
.getObject("countText");
|
||||
|
||||
try {
|
||||
return Integer.parseInt(
|
||||
Utils.removeNonDigitCharacters(getTextFromObject(countText))
|
||||
);
|
||||
} catch (final Exception e) {
|
||||
throw new ExtractionException("Unable to get comments count", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import java.io.IOException;
|
|||
import javax.annotation.Nonnull;
|
||||
|
||||
public class YoutubeFeedExtractor extends FeedExtractor {
|
||||
private static final String WEBSITE_CHANNEL_BASE_URL = "https://www.youtube.com/channel/";
|
||||
|
||||
public YoutubeFeedExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
@ -57,19 +59,40 @@ public class YoutubeFeedExtractor extends FeedExtractor {
|
|||
@Nonnull
|
||||
@Override
|
||||
public String getId() {
|
||||
return document.getElementsByTag("yt:channelId").first().text();
|
||||
return getUrl().replace(WEBSITE_CHANNEL_BASE_URL, "");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return document.select("feed > author > uri").first().text();
|
||||
final Element authorUriElement = document.select("feed > author > uri")
|
||||
.first();
|
||||
if (authorUriElement != null) {
|
||||
final String authorUriElementText = authorUriElement.text();
|
||||
if (!authorUriElementText.equals("")) {
|
||||
return authorUriElementText;
|
||||
}
|
||||
}
|
||||
|
||||
final Element linkElement = document.select("feed > link[rel*=alternate]")
|
||||
.first();
|
||||
if (linkElement != null) {
|
||||
return linkElement.attr("href");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() {
|
||||
return document.select("feed > author > name").first().text();
|
||||
final Element nameElement = document.select("feed > author > name")
|
||||
.first();
|
||||
if (nameElement == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return nameElement.text();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,11 +2,11 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
|
|||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addYouTubeHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.extractCookieValue;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.extractPlaylistTypeFromPlaylistId;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getYouTubeHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.getQueryValue;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
@ -88,12 +88,12 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||
|
||||
final byte[] body = JsonWriter.string(jsonBody.done()).getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
// Cookie is required due to consent
|
||||
addYouTubeHeaders(headers);
|
||||
final var headers = getYouTubeHeaders();
|
||||
|
||||
final Response response = getDownloader().post(YOUTUBEI_V1_URL + "next?key=" + getKey()
|
||||
+ DISABLE_PRETTY_PRINT_PARAMETER, headers, body, localization);
|
||||
final Response response = getDownloader().postWithContentTypeJson(
|
||||
YOUTUBEI_V1_URL + "next?key=" + getKey() + DISABLE_PRETTY_PRINT_PARAMETER,
|
||||
headers, body, localization);
|
||||
|
||||
initialData = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
playlistData = initialData
|
||||
|
@ -221,12 +221,11 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||
}
|
||||
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
// Cookie is required due to consent
|
||||
addYouTubeHeaders(headers);
|
||||
final var headers = getYouTubeHeaders();
|
||||
|
||||
final Response response = getDownloader().post(page.getUrl(), headers, page.getBody(),
|
||||
getExtractorLocalization());
|
||||
final Response response = getDownloader().postWithContentTypeJson(page.getUrl(), headers,
|
||||
page.getBody(), getExtractorLocalization());
|
||||
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
final JsonObject playlistJson = ajaxJson.getObject("contents")
|
||||
.getObject("twoColumnWatchNextResults").getObject("playlist").getObject("playlist");
|
||||
|
|
|
@ -5,6 +5,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
|||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getYoutubeMusicHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_ALBUMS;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_ARTISTS;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_PLAYLISTS;
|
||||
|
@ -39,9 +40,7 @@ import org.schabi.newpipe.extractor.utils.Utils;
|
|||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
@ -116,15 +115,8 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||
.end().done().getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("X-YouTube-Client-Name", Collections.singletonList(youtubeMusicKeys[1]));
|
||||
headers.put("X-YouTube-Client-Version", Collections.singletonList(youtubeMusicKeys[2]));
|
||||
headers.put("Origin", Collections.singletonList("https://music.youtube.com"));
|
||||
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||
|
||||
final String responseBody = getValidJsonResponseBody(getDownloader().post(url, headers,
|
||||
json));
|
||||
final String responseBody = getValidJsonResponseBody(
|
||||
getDownloader().postWithContentTypeJson(url, getYoutubeMusicHeaders(), json));
|
||||
|
||||
try {
|
||||
initialData = JsonParser.object().from(responseBody);
|
||||
|
@ -251,15 +243,9 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||
.end().done().getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("X-YouTube-Client-Name", Collections.singletonList(youtubeMusicKeys[1]));
|
||||
headers.put("X-YouTube-Client-Version", Collections.singletonList(youtubeMusicKeys[2]));
|
||||
headers.put("Origin", Collections.singletonList("https://music.youtube.com"));
|
||||
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||
|
||||
final String responseBody = getValidJsonResponseBody(getDownloader().post(page.getUrl(),
|
||||
headers, json));
|
||||
final String responseBody = getValidJsonResponseBody(
|
||||
getDownloader().postWithContentTypeJson(
|
||||
page.getUrl(), getYoutubeMusicHeaders(), json));
|
||||
|
||||
final JsonObject ajaxJson;
|
||||
try {
|
||||
|
|
|
@ -2,14 +2,12 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
|
|||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.extractPlaylistTypeFromPlaylistUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
|
@ -20,7 +18,6 @@ import com.grack.nanojson.JsonWriter;
|
|||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
@ -31,14 +28,10 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -349,12 +342,9 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||
}
|
||||
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
addClientInfoHeaders(headers);
|
||||
|
||||
final Response response = getDownloader().post(page.getUrl(), headers, page.getBody(),
|
||||
final JsonObject ajaxJson = getJsonPostResponse("browse", page.getBody(),
|
||||
getExtractorLocalization());
|
||||
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||
|
||||
final JsonArray continuation = ajaxJson.getArray("onResponseReceivedActions")
|
||||
.getObject(0)
|
||||
|
@ -403,12 +393,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||
.map(JsonObject.class::cast)
|
||||
.filter(video -> video.has(PLAYLIST_VIDEO_RENDERER))
|
||||
.map(video -> new YoutubeStreamInfoItemExtractor(
|
||||
video.getObject(PLAYLIST_VIDEO_RENDERER), timeAgoParser) {
|
||||
@Override
|
||||
public long getViewCount() {
|
||||
return -1;
|
||||
}
|
||||
})
|
||||
video.getObject(PLAYLIST_VIDEO_RENDERER), timeAgoParser))
|
||||
.forEachOrdered(collector::commit);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
|||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.getSearchParameter;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
@ -13,8 +12,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonBuilder;
|
||||
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.InfoItem;
|
||||
|
@ -34,10 +31,10 @@ import org.schabi.newpipe.extractor.utils.JsonUtils;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 22.07.2018
|
||||
|
@ -105,10 +102,14 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||
@Override
|
||||
public String getSearchSuggestion() throws ParsingException {
|
||||
final JsonObject itemSectionRenderer = initialData.getObject("contents")
|
||||
.getObject("twoColumnSearchResultsRenderer").getObject("primaryContents")
|
||||
.getObject("sectionListRenderer").getArray("contents").getObject(0)
|
||||
.getObject("twoColumnSearchResultsRenderer")
|
||||
.getObject("primaryContents")
|
||||
.getObject("sectionListRenderer")
|
||||
.getArray("contents")
|
||||
.getObject(0)
|
||||
.getObject("itemSectionRenderer");
|
||||
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents").getObject(0)
|
||||
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents")
|
||||
.getObject(0)
|
||||
.getObject("didYouMeanRenderer");
|
||||
final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents")
|
||||
.getObject(0)
|
||||
|
@ -138,8 +139,10 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||
@Override
|
||||
public List<MetaInfo> getMetaInfo() throws ParsingException {
|
||||
return YoutubeParsingHelper.getMetaInfo(
|
||||
initialData.getObject("contents").getObject("twoColumnSearchResultsRenderer")
|
||||
.getObject("primaryContents").getObject("sectionListRenderer")
|
||||
initialData.getObject("contents")
|
||||
.getObject("twoColumnSearchResultsRenderer")
|
||||
.getObject("primaryContents")
|
||||
.getObject("sectionListRenderer")
|
||||
.getArray("contents"));
|
||||
}
|
||||
|
||||
|
@ -149,20 +152,23 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
||||
|
||||
final JsonArray sections = initialData.getObject("contents")
|
||||
.getObject("twoColumnSearchResultsRenderer").getObject("primaryContents")
|
||||
.getObject("sectionListRenderer").getArray("contents");
|
||||
.getObject("twoColumnSearchResultsRenderer")
|
||||
.getObject("primaryContents")
|
||||
.getObject("sectionListRenderer")
|
||||
.getArray("contents");
|
||||
|
||||
Page nextPage = null;
|
||||
|
||||
for (final Object section : sections) {
|
||||
if (((JsonObject) section).has("itemSectionRenderer")) {
|
||||
final JsonObject itemSectionRenderer = ((JsonObject) section)
|
||||
.getObject("itemSectionRenderer");
|
||||
final JsonObject sectionJsonObject = (JsonObject) section;
|
||||
if (sectionJsonObject.has("itemSectionRenderer")) {
|
||||
final JsonObject itemSectionRenderer =
|
||||
sectionJsonObject.getObject("itemSectionRenderer");
|
||||
|
||||
collectStreamsFrom(collector, itemSectionRenderer.getArray("contents"));
|
||||
} else if (((JsonObject) section).has("continuationItemRenderer")) {
|
||||
nextPage = getNextPageFrom(((JsonObject) section)
|
||||
.getObject("continuationItemRenderer"));
|
||||
} else if (sectionJsonObject.has("continuationItemRenderer")) {
|
||||
nextPage = getNextPageFrom(
|
||||
sectionJsonObject.getObject("continuationItemRenderer"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,22 +193,16 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||
.getBytes(StandardCharsets.UTF_8);
|
||||
// @formatter:on
|
||||
|
||||
final String responseBody = getValidJsonResponseBody(getDownloader().post(
|
||||
page.getUrl(), new HashMap<>(), json));
|
||||
|
||||
final JsonObject ajaxJson;
|
||||
try {
|
||||
ajaxJson = JsonParser.object().from(responseBody);
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ParsingException("Could not parse JSON", e);
|
||||
}
|
||||
final JsonObject ajaxJson = getJsonPostResponse("search", json, localization);
|
||||
|
||||
final JsonArray continuationItems = ajaxJson.getArray("onResponseReceivedCommands")
|
||||
.getObject(0).getObject("appendContinuationItemsAction")
|
||||
.getObject(0)
|
||||
.getObject("appendContinuationItemsAction")
|
||||
.getArray("continuationItems");
|
||||
|
||||
final JsonArray contents = continuationItems.getObject(0)
|
||||
.getObject("itemSectionRenderer").getArray("contents");
|
||||
.getObject("itemSectionRenderer")
|
||||
.getArray("contents");
|
||||
collectStreamsFrom(collector, contents);
|
||||
|
||||
return new InfoItemsPage<>(collector, getNextPageFrom(continuationItems.getObject(1)
|
||||
|
@ -210,28 +210,30 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||
}
|
||||
|
||||
private void collectStreamsFrom(final MultiInfoItemsCollector collector,
|
||||
final JsonArray contents) throws NothingFoundException,
|
||||
ParsingException {
|
||||
@Nonnull final JsonArray contents)
|
||||
throws NothingFoundException, ParsingException {
|
||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||
|
||||
for (final Object content : contents) {
|
||||
final JsonObject item = (JsonObject) content;
|
||||
if (item.has("backgroundPromoRenderer")) {
|
||||
throw new NothingFoundException(getTextFromObject(
|
||||
item.getObject("backgroundPromoRenderer").getObject("bodyText")));
|
||||
throw new NothingFoundException(
|
||||
getTextFromObject(item.getObject("backgroundPromoRenderer")
|
||||
.getObject("bodyText")));
|
||||
} else if (item.has("videoRenderer")) {
|
||||
collector.commit(new YoutubeStreamInfoItemExtractor(item
|
||||
.getObject("videoRenderer"), timeAgoParser));
|
||||
collector.commit(new YoutubeStreamInfoItemExtractor(
|
||||
item.getObject("videoRenderer"), timeAgoParser));
|
||||
} else if (item.has("channelRenderer")) {
|
||||
collector.commit(new YoutubeChannelInfoItemExtractor(item
|
||||
.getObject("channelRenderer")));
|
||||
collector.commit(new YoutubeChannelInfoItemExtractor(
|
||||
item.getObject("channelRenderer")));
|
||||
} else if (item.has("playlistRenderer")) {
|
||||
collector.commit(new YoutubePlaylistInfoItemExtractor(item
|
||||
.getObject("playlistRenderer")));
|
||||
collector.commit(new YoutubePlaylistInfoItemExtractor(
|
||||
item.getObject("playlistRenderer")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Page getNextPageFrom(final JsonObject continuationItemRenderer) throws IOException,
|
||||
ExtractionException {
|
||||
if (isNullOrEmpty(continuationItemRenderer)) {
|
||||
|
@ -239,7 +241,8 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||
}
|
||||
|
||||
final String token = continuationItemRenderer.getObject("continuationEndpoint")
|
||||
.getObject("continuationCommand").getString("token");
|
||||
.getObject("continuationCommand")
|
||||
.getString("token");
|
||||
|
||||
final String url = YOUTUBEI_V1_URL + "search?key=" + getKey()
|
||||
+ DISABLE_PRETTY_PRINT_PARAMETER;
|
||||
|
|
|
@ -82,6 +82,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
|||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
import org.schabi.newpipe.extractor.utils.Pair;
|
||||
import org.schabi.newpipe.extractor.utils.Parser;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
@ -168,11 +169,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
title = playerResponse.getObject("videoDetails").getString("title");
|
||||
|
||||
if (isNullOrEmpty(title)) {
|
||||
try {
|
||||
title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
|
||||
} catch (final ParsingException ignored) {
|
||||
// Age-restricted videos cause a ParsingException here
|
||||
}
|
||||
title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
|
||||
|
||||
if (isNullOrEmpty(title)) {
|
||||
throw new ParsingException("Could not get name");
|
||||
|
@ -285,21 +282,17 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
public Description getDescription() throws ParsingException {
|
||||
assertPageFetched();
|
||||
// Description with more info on links
|
||||
try {
|
||||
final String description = getTextFromObject(
|
||||
getVideoSecondaryInfoRenderer().getObject("description"),
|
||||
true);
|
||||
if (!isNullOrEmpty(description)) {
|
||||
return new Description(description, Description.HTML);
|
||||
}
|
||||
final String videoSecondaryInfoRendererDescription = getTextFromObject(
|
||||
getVideoSecondaryInfoRenderer().getObject("description"),
|
||||
true);
|
||||
if (!isNullOrEmpty(videoSecondaryInfoRendererDescription)) {
|
||||
return new Description(videoSecondaryInfoRendererDescription, Description.HTML);
|
||||
}
|
||||
|
||||
final String attributedDescription = getAttributedDescription(
|
||||
getVideoSecondaryInfoRenderer().getObject("attributedDescription"));
|
||||
if (!isNullOrEmpty(attributedDescription)) {
|
||||
return new Description(attributedDescription, Description.HTML);
|
||||
}
|
||||
} catch (final ParsingException ignored) {
|
||||
// Age-restricted videos cause a ParsingException here
|
||||
final String attributedDescription = getAttributedDescription(
|
||||
getVideoSecondaryInfoRenderer().getObject("attributedDescription"));
|
||||
if (!isNullOrEmpty(attributedDescription)) {
|
||||
return new Description(attributedDescription, Description.HTML);
|
||||
}
|
||||
|
||||
String description = playerResponse.getObject("videoDetails")
|
||||
|
@ -400,14 +393,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
String views = null;
|
||||
|
||||
try {
|
||||
views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
|
||||
.getObject("videoViewCountRenderer").getObject("viewCount"));
|
||||
} catch (final ParsingException ignored) {
|
||||
// Age-restricted videos cause a ParsingException here
|
||||
}
|
||||
String views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
|
||||
.getObject("videoViewCountRenderer").getObject("viewCount"));
|
||||
|
||||
if (isNullOrEmpty(views)) {
|
||||
views = playerResponse.getObject("videoDetails").getString("viewCount");
|
||||
|
@ -795,7 +782,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
return getTextFromObject(playerResponse.getObject("playabilityStatus")
|
||||
.getObject("errorScreen").getObject("playerErrorMessageRenderer")
|
||||
.getObject("reason"));
|
||||
} catch (final ParsingException | NullPointerException e) {
|
||||
} catch (final NullPointerException e) {
|
||||
return null; // No error message
|
||||
}
|
||||
}
|
||||
|
@ -1323,6 +1310,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
.setAverageBitrate(itagItem.getAverageBitrate())
|
||||
.setAudioTrackId(itagItem.getAudioTrackId())
|
||||
.setAudioTrackName(itagItem.getAudioTrackName())
|
||||
.setAudioLocale(itagItem.getAudioLocale())
|
||||
.setIsDescriptive(itagItem.isDescriptiveAudio())
|
||||
.setItagItem(itagItem);
|
||||
|
||||
if (streamType == StreamType.LIVE_STREAM
|
||||
|
@ -1468,9 +1457,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
itagItem.setQuality(formatData.getString("quality"));
|
||||
itagItem.setCodec(codec);
|
||||
|
||||
itagItem.setAudioTrackId(formatData.getObject("audioTrack").getString("id"));
|
||||
itagItem.setAudioTrackName(formatData.getObject("audioTrack").getString("displayName"));
|
||||
|
||||
if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) {
|
||||
itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec"));
|
||||
}
|
||||
|
@ -1487,6 +1473,27 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
// AudioChannelConfiguration element of DASH manifests of audio streams in
|
||||
// YoutubeDashManifestCreatorUtils
|
||||
2));
|
||||
|
||||
final String audioTrackId = formatData.getObject("audioTrack")
|
||||
.getString("id");
|
||||
if (!isNullOrEmpty(audioTrackId)) {
|
||||
itagItem.setAudioTrackId(audioTrackId);
|
||||
final int audioTrackIdLastLocaleCharacter = audioTrackId.indexOf(".");
|
||||
if (audioTrackIdLastLocaleCharacter != -1) {
|
||||
// Audio tracks IDs are in the form LANGUAGE_CODE.TRACK_NUMBER
|
||||
itagItem.setAudioLocale(LocaleCompat.forLanguageTag(
|
||||
audioTrackId.substring(0, audioTrackIdLastLocaleCharacter)));
|
||||
}
|
||||
}
|
||||
|
||||
itagItem.setAudioTrackName(formatData.getObject("audioTrack")
|
||||
.getString("displayName"));
|
||||
|
||||
// Descriptive audio tracks
|
||||
// This information is also provided as a protobuf object in the formatData
|
||||
itagItem.setIsDescriptiveAudio(streamUrl.contains("acont%3Ddescriptive")
|
||||
// Support "decoded" URLs
|
||||
|| streamUrl.contains("acont=descriptive"));
|
||||
}
|
||||
|
||||
// YouTube return the content length and the approximate duration as strings
|
||||
|
|
|
@ -10,13 +10,16 @@ import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLi
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.Parser;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getThumbnailUrlFromInfoItem;
|
||||
|
@ -42,9 +45,15 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|||
*/
|
||||
|
||||
public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
||||
|
||||
private static final Pattern ACCESSIBILITY_DATA_VIEW_COUNT_REGEX =
|
||||
Pattern.compile("([\\d,]+) views$");
|
||||
private static final String NO_VIEWS_LOWERCASE = "no views";
|
||||
|
||||
private final JsonObject videoInfo;
|
||||
private final TimeAgoParser timeAgoParser;
|
||||
private StreamType cachedStreamType;
|
||||
private Boolean isPremiere;
|
||||
|
||||
/**
|
||||
* Creates an extractor of StreamInfoItems from a YouTube page.
|
||||
|
@ -66,6 +75,10 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
|
||||
final JsonArray badges = videoInfo.getArray("badges");
|
||||
for (final Object badge : badges) {
|
||||
if (!(badge instanceof JsonObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final JsonObject badgeRenderer
|
||||
= ((JsonObject) badge).getObject("metadataBadgeRenderer");
|
||||
if (badgeRenderer.getString("style", "").equals("BADGE_STYLE_TYPE_LIVE_NOW")
|
||||
|
@ -76,6 +89,10 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
}
|
||||
|
||||
for (final Object overlay : videoInfo.getArray("thumbnailOverlays")) {
|
||||
if (!(overlay instanceof JsonObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String style = ((JsonObject) overlay)
|
||||
.getObject("thumbnailOverlayTimeStatusRenderer")
|
||||
.getString("style", "");
|
||||
|
@ -122,21 +139,40 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
|
||||
@Override
|
||||
public long getDuration() throws ParsingException {
|
||||
if (getStreamType() == StreamType.LIVE_STREAM || isPremiere()) {
|
||||
if (getStreamType() == StreamType.LIVE_STREAM) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
String duration = getTextFromObject(videoInfo.getObject("lengthText"));
|
||||
|
||||
if (isNullOrEmpty(duration)) {
|
||||
for (final Object thumbnailOverlay : videoInfo.getArray("thumbnailOverlays")) {
|
||||
if (((JsonObject) thumbnailOverlay).has("thumbnailOverlayTimeStatusRenderer")) {
|
||||
duration = getTextFromObject(((JsonObject) thumbnailOverlay)
|
||||
.getObject("thumbnailOverlayTimeStatusRenderer").getObject("text"));
|
||||
// Available in playlists for videos
|
||||
duration = videoInfo.getString("lengthSeconds");
|
||||
|
||||
if (isNullOrEmpty(duration)) {
|
||||
final JsonObject timeOverlay = videoInfo.getArray("thumbnailOverlays")
|
||||
.stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.filter(thumbnailOverlay ->
|
||||
thumbnailOverlay.has("thumbnailOverlayTimeStatusRenderer"))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (timeOverlay != null) {
|
||||
duration = getTextFromObject(
|
||||
timeOverlay.getObject("thumbnailOverlayTimeStatusRenderer")
|
||||
.getObject("text"));
|
||||
}
|
||||
}
|
||||
|
||||
if (isNullOrEmpty(duration)) {
|
||||
if (isPremiere()) {
|
||||
// Premieres can be livestreams, so the duration is not available in this
|
||||
// case
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Duration of short videos in channel tab
|
||||
// example: "simple is best - 49 seconds - play video"
|
||||
final String accessibilityLabel = videoInfo.getObject("accessibility")
|
||||
|
@ -150,15 +186,10 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
if (labelParts.length > 2) {
|
||||
final String textualDuration = labelParts[labelParts.length - 2];
|
||||
return timeAgoParser.parseDuration(textualDuration);
|
||||
} else {
|
||||
throw new ParsingException("Could not get duration");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewPipe#8034 - YT returns not a correct duration for "YT shorts" videos
|
||||
if ("SHORTS".equalsIgnoreCase(duration)) {
|
||||
return 0;
|
||||
throw new ParsingException("Could not get duration");
|
||||
}
|
||||
}
|
||||
|
||||
return YoutubeParsingHelper.parseDurationString(duration);
|
||||
|
@ -208,10 +239,9 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
@Nullable
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() throws ParsingException {
|
||||
|
||||
if (videoInfo.has("channelThumbnailSupportedRenderers")) {
|
||||
return JsonUtils.getArray(videoInfo, "channelThumbnailSupportedRenderers"
|
||||
+ ".channelThumbnailWithLinkRenderer.thumbnail.thumbnails")
|
||||
+ ".channelThumbnailWithLinkRenderer.thumbnail.thumbnails")
|
||||
.getObject(0).getString("url");
|
||||
}
|
||||
|
||||
|
@ -239,25 +269,30 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(getDateFromPremiere());
|
||||
}
|
||||
|
||||
final String publishedTimeText
|
||||
= getTextFromObject(videoInfo.getObject("publishedTimeText"));
|
||||
if (!isNullOrEmpty(publishedTimeText)) {
|
||||
return publishedTimeText;
|
||||
String publishedTimeText = getTextFromObject(videoInfo.getObject("publishedTimeText"));
|
||||
|
||||
if (isNullOrEmpty(publishedTimeText) && videoInfo.has("videoInfo")) {
|
||||
/*
|
||||
Returned in playlists, in the form: view count separator upload date
|
||||
*/
|
||||
publishedTimeText = videoInfo.getObject("videoInfo")
|
||||
.getArray("runs")
|
||||
.getObject(2)
|
||||
.getString("text");
|
||||
}
|
||||
|
||||
final String shortsTimestampText = getTextFromObject(videoInfo
|
||||
.getObject("navigationEndpoint")
|
||||
.getObject("reelWatchEndpoint").getObject("overlay")
|
||||
.getObject("reelPlayerOverlayRenderer")
|
||||
.getObject("reelPlayerHeaderSupportedRenderers")
|
||||
.getObject("reelPlayerHeaderRenderer")
|
||||
.getObject("timestampText")
|
||||
);
|
||||
if (!isNullOrEmpty(shortsTimestampText)) {
|
||||
return shortsTimestampText;
|
||||
if (isNullOrEmpty(publishedTimeText)) {
|
||||
publishedTimeText = getTextFromObject(videoInfo
|
||||
.getObject("navigationEndpoint")
|
||||
.getObject("reelWatchEndpoint").getObject("overlay")
|
||||
.getObject("reelPlayerOverlayRenderer")
|
||||
.getObject("reelPlayerHeaderSupportedRenderers")
|
||||
.getObject("reelPlayerHeaderRenderer")
|
||||
.getObject("timestampText")
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return isNullOrEmpty(publishedTimeText) ? null : publishedTimeText;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -284,28 +319,88 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
try {
|
||||
if (videoInfo.has("topStandaloneBadge") || isPremium()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!videoInfo.has("viewCountText")) {
|
||||
// This object is null when a video has its views hidden.
|
||||
return -1;
|
||||
}
|
||||
|
||||
final String viewCount = getTextFromObject(videoInfo.getObject("viewCountText"));
|
||||
|
||||
if (viewCount.toLowerCase().contains("no views")) {
|
||||
return 0;
|
||||
} else if (viewCount.toLowerCase().contains("recommended")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return Long.parseLong(Utils.removeNonDigitCharacters(viewCount));
|
||||
} catch (final Exception e) {
|
||||
throw new ParsingException("Could not get view count", e);
|
||||
if (isPremium() || isPremiere()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Ignore all exceptions, as the view count can be hidden by creators, and so cannot be
|
||||
// found in this case
|
||||
|
||||
final String viewCountText = getTextFromObject(videoInfo.getObject("viewCountText"));
|
||||
if (!isNullOrEmpty(viewCountText)) {
|
||||
try {
|
||||
return getViewCountFromViewCountText(viewCountText, false);
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing the real view count from accessibility data, if that's not a running
|
||||
// livestream (the view count is returned and not the count of people watching currently
|
||||
// the livestream)
|
||||
if (getStreamType() != StreamType.LIVE_STREAM) {
|
||||
try {
|
||||
return getViewCountFromAccessibilityData();
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to a short view count, always used for livestreams (see why above)
|
||||
if (videoInfo.has("videoInfo")) {
|
||||
// Returned in playlists, in the form: view count separator upload date
|
||||
try {
|
||||
return getViewCountFromViewCountText(videoInfo.getObject("videoInfo")
|
||||
.getArray("runs")
|
||||
.getObject(0)
|
||||
.getString("text", ""), true);
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (videoInfo.has("shortViewCountText")) {
|
||||
// Returned everywhere but in playlists, used by the website to show view counts
|
||||
try {
|
||||
final String shortViewCountText =
|
||||
getTextFromObject(videoInfo.getObject("shortViewCountText"));
|
||||
if (!isNullOrEmpty(shortViewCountText)) {
|
||||
return getViewCountFromViewCountText(shortViewCountText, true);
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// No view count extracted: return -1, as the view count can be hidden by creators on videos
|
||||
return -1;
|
||||
}
|
||||
|
||||
private long getViewCountFromViewCountText(@Nonnull final String viewCountText,
|
||||
final boolean isMixedNumber)
|
||||
throws NumberFormatException, ParsingException {
|
||||
// These approaches are language dependent
|
||||
if (viewCountText.toLowerCase().contains(NO_VIEWS_LOWERCASE)) {
|
||||
return 0;
|
||||
} else if (viewCountText.toLowerCase().contains("recommended")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return isMixedNumber ? Utils.mixedNumberWordToLong(viewCountText)
|
||||
: Long.parseLong(Utils.removeNonDigitCharacters(viewCountText));
|
||||
}
|
||||
|
||||
private long getViewCountFromAccessibilityData()
|
||||
throws NumberFormatException, Parser.RegexException {
|
||||
// These approaches are language dependent
|
||||
final String videoInfoTitleAccessibilityData = videoInfo.getObject("title")
|
||||
.getObject("accessibility")
|
||||
.getObject("accessibilityData")
|
||||
.getString("label", "");
|
||||
|
||||
if (videoInfoTitleAccessibilityData.toLowerCase().endsWith(NO_VIEWS_LOWERCASE)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Long.parseLong(Utils.removeNonDigitCharacters(
|
||||
Parser.matchGroup1(ACCESSIBILITY_DATA_VIEW_COUNT_REGEX,
|
||||
videoInfoTitleAccessibilityData)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -325,7 +420,10 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
|||
}
|
||||
|
||||
private boolean isPremiere() {
|
||||
return videoInfo.has("upcomingEventData");
|
||||
if (isPremiere == null) {
|
||||
isPremiere = videoInfo.has("upcomingEventData");
|
||||
}
|
||||
return isPremiere;
|
||||
}
|
||||
|
||||
private OffsetDateTime getDateFromPremiere() throws ParsingException {
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
/*
|
||||
* Created by Christian Schabesberger on 02.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeStreamLinkHandlerFactory.java is part of NewPipe Extractor.
|
||||
*
|
||||
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isHooktubeURL;
|
||||
|
@ -15,33 +35,13 @@ import java.net.MalformedURLException;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 02.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeStreamLinkHandlerFactory.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
||||
|
||||
private static final Pattern YOUTUBE_VIDEO_ID_REGEX_PATTERN
|
||||
|
@ -49,7 +49,7 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
|||
private static final YoutubeStreamLinkHandlerFactory INSTANCE
|
||||
= new YoutubeStreamLinkHandlerFactory();
|
||||
private static final List<String> SUBPATHS
|
||||
= Arrays.asList("embed/", "shorts/", "watch/", "v/", "w/");
|
||||
= List.of("embed/", "live/", "shorts/", "watch/", "v/", "w/");
|
||||
|
||||
private YoutubeStreamLinkHandlerFactory() {
|
||||
}
|
||||
|
@ -67,21 +67,24 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String assertIsId(@Nullable final String id) throws ParsingException {
|
||||
final String extractedId = extractId(id);
|
||||
if (extractedId != null) {
|
||||
return extractedId;
|
||||
} else {
|
||||
throw new ParsingException("The given string is not a Youtube-Video-ID");
|
||||
throw new ParsingException("The given string is not a YouTube video ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUrl(final String id) {
|
||||
return "https://www.youtube.com/watch?v=" + id;
|
||||
}
|
||||
|
||||
@SuppressWarnings("AvoidNestedBlocks")
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getId(final String theUrlString)
|
||||
throws ParsingException, IllegalArgumentException {
|
||||
|
@ -124,14 +127,14 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
|||
if (!Utils.isHTTP(url) || !(isYoutubeURL(url) || isYoutubeServiceURL(url)
|
||||
|| isHooktubeURL(url) || isInvidiousURL(url) || isY2ubeURL(url))) {
|
||||
if (host.equalsIgnoreCase("googleads.g.doubleclick.net")) {
|
||||
throw new FoundAdException("Error found ad: " + urlString);
|
||||
throw new FoundAdException("Error: found ad: " + urlString);
|
||||
}
|
||||
|
||||
throw new ParsingException("The url is not a Youtube-URL");
|
||||
throw new ParsingException("The URL is not a YouTube URL");
|
||||
}
|
||||
|
||||
if (YoutubePlaylistLinkHandlerFactory.getInstance().acceptUrl(urlString)) {
|
||||
throw new ParsingException("Error no suitable url: " + urlString);
|
||||
throw new ParsingException("Error: no suitable URL: " + urlString);
|
||||
}
|
||||
|
||||
// Using uppercase instead of lowercase, because toLowercase replaces some unicode
|
||||
|
@ -154,9 +157,9 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
|||
|
||||
final URL decodedURL;
|
||||
try {
|
||||
decodedURL = Utils.stringToURL("http://www.youtube.com" + uQueryValue);
|
||||
decodedURL = Utils.stringToURL("https://www.youtube.com" + uQueryValue);
|
||||
} catch (final MalformedURLException e) {
|
||||
throw new ParsingException("Error no suitable url: " + urlString);
|
||||
throw new ParsingException("Error: no suitable URL: " + urlString);
|
||||
}
|
||||
|
||||
final String viewQueryValue = Utils.getQueryValue(decodedURL, "v");
|
||||
|
@ -231,7 +234,7 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
|||
}
|
||||
}
|
||||
|
||||
throw new ParsingException("Error no suitable url: " + urlString);
|
||||
throw new ParsingException("Error: no suitable URL: " + urlString);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -246,7 +249,8 @@ public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private String getIdFromSubpathsInPath(final String path) throws ParsingException {
|
||||
@Nullable
|
||||
private String getIdFromSubpathsInPath(@Nonnull final String path) throws ParsingException {
|
||||
for (final String subpath : SUBPATHS) {
|
||||
if (path.startsWith(subpath)) {
|
||||
final String id = path.substring(subpath.length());
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
|||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AudioStream extends Stream {
|
||||
|
@ -43,8 +44,14 @@ public final class AudioStream extends Stream {
|
|||
private String codec;
|
||||
|
||||
// Fields about the audio track id/name
|
||||
private String audioTrackId;
|
||||
private String audioTrackName;
|
||||
@Nullable
|
||||
private final String audioTrackId;
|
||||
@Nullable
|
||||
private final String audioTrackName;
|
||||
@Nullable
|
||||
private final Locale audioLocale;
|
||||
private final boolean isDescriptive;
|
||||
|
||||
@Nullable
|
||||
private ItagItem itagItem;
|
||||
|
||||
|
@ -67,6 +74,9 @@ public final class AudioStream extends Stream {
|
|||
@Nullable
|
||||
private String audioTrackName;
|
||||
@Nullable
|
||||
private Locale audioLocale;
|
||||
private boolean isDescriptive;
|
||||
@Nullable
|
||||
private ItagItem itagItem;
|
||||
|
||||
/**
|
||||
|
@ -185,7 +195,11 @@ public final class AudioStream extends Stream {
|
|||
/**
|
||||
* Set the audio track id of the {@link AudioStream}.
|
||||
*
|
||||
* @param audioTrackId the audio track id of the {@link AudioStream}
|
||||
* <p>
|
||||
* The default value is {@code null}.
|
||||
* </p>
|
||||
*
|
||||
* @param audioTrackId the audio track id of the {@link AudioStream}, which can be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setAudioTrackId(@Nullable final String audioTrackId) {
|
||||
|
@ -196,7 +210,11 @@ public final class AudioStream extends Stream {
|
|||
/**
|
||||
* Set the audio track name of the {@link AudioStream}.
|
||||
*
|
||||
* @param audioTrackName the audio track name of the {@link AudioStream}
|
||||
* <p>
|
||||
* The default value is {@code null}.
|
||||
* </p>
|
||||
*
|
||||
* @param audioTrackName the audio track name of the {@link AudioStream}, which can be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setAudioTrackName(@Nullable final String audioTrackName) {
|
||||
|
@ -204,6 +222,44 @@ public final class AudioStream extends Stream {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this {@link AudioStream} is a descriptive audio.
|
||||
*
|
||||
* <p>
|
||||
* A descriptive audio is an audio in which descriptions of visual elements of a video are
|
||||
* added in the original audio, with the goal to make a video more accessible to blind and
|
||||
* visually impaired people.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The default value is {@code false}.
|
||||
* </p>
|
||||
*
|
||||
* @param isDescriptive whether this {@link AudioStream} is a descriptive audio
|
||||
* @return this {@link Builder} instance
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Audio_description">
|
||||
* https://en.wikipedia.org/wiki/Audio_description</a>
|
||||
*/
|
||||
public Builder setIsDescriptive(final boolean isDescriptive) {
|
||||
this.isDescriptive = isDescriptive;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link Locale} of the audio which represents its language.
|
||||
*
|
||||
* <p>
|
||||
* The default value is {@code null}, which means that the {@link Locale} is unknown.
|
||||
* </p>
|
||||
*
|
||||
* @param audioLocale the {@link Locale} of the audio, which could be {@code null}
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setAudioLocale(@Nullable final Locale audioLocale) {
|
||||
this.audioLocale = audioLocale;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ItagItem} corresponding to the {@link AudioStream}.
|
||||
*
|
||||
|
@ -257,7 +313,8 @@ public final class AudioStream extends Stream {
|
|||
}
|
||||
|
||||
return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate,
|
||||
manifestUrl, audioTrackId, audioTrackName, itagItem);
|
||||
manifestUrl, audioTrackId, audioTrackName, audioLocale, isDescriptive,
|
||||
itagItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,6 +334,7 @@ public final class AudioStream extends Stream {
|
|||
* {@link #UNKNOWN_BITRATE})
|
||||
* @param audioTrackId the id of the audio track
|
||||
* @param audioTrackName the name of the audio track
|
||||
* @param audioLocale the {@link Locale} of the audio stream, representing its language
|
||||
* @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null
|
||||
* @param manifestUrl the URL of the manifest this stream comes from (if applicable,
|
||||
* otherwise null)
|
||||
|
@ -291,6 +349,8 @@ public final class AudioStream extends Stream {
|
|||
@Nullable final String manifestUrl,
|
||||
@Nullable final String audioTrackId,
|
||||
@Nullable final String audioTrackName,
|
||||
@Nullable final Locale audioLocale,
|
||||
final boolean isDescriptive,
|
||||
@Nullable final ItagItem itagItem) {
|
||||
super(id, content, isUrl, format, deliveryMethod, manifestUrl);
|
||||
if (itagItem != null) {
|
||||
|
@ -307,6 +367,8 @@ public final class AudioStream extends Stream {
|
|||
this.averageBitrate = averageBitrate;
|
||||
this.audioTrackId = audioTrackId;
|
||||
this.audioTrackName = audioTrackName;
|
||||
this.audioLocale = audioLocale;
|
||||
this.isDescriptive = isDescriptive;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -316,7 +378,9 @@ public final class AudioStream extends Stream {
|
|||
public boolean equalStats(final Stream cmp) {
|
||||
return super.equalStats(cmp) && cmp instanceof AudioStream
|
||||
&& averageBitrate == ((AudioStream) cmp).averageBitrate
|
||||
&& Objects.equals(audioTrackId, ((AudioStream) cmp).audioTrackId);
|
||||
&& Objects.equals(audioTrackId, ((AudioStream) cmp).audioTrackId)
|
||||
&& isDescriptive == ((AudioStream) cmp).isDescriptive
|
||||
&& Objects.equals(audioLocale, ((AudioStream) cmp).audioLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -421,15 +485,44 @@ public final class AudioStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the name of the audio track.
|
||||
* Get the name of the audio track, which may be {@code null} if this information is not
|
||||
* provided by the service.
|
||||
*
|
||||
* @return the name of the audio track
|
||||
* @return the name of the audio track or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public String getAudioTrackName() {
|
||||
return audioTrackName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Locale} of the audio representing the language of the stream, which is
|
||||
* {@code null} if the audio language of this stream is not known.
|
||||
*
|
||||
* @return the {@link Locale} of the audio or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public Locale getAudioLocale() {
|
||||
return audioLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this stream is a descriptive audio.
|
||||
*
|
||||
* <p>
|
||||
* A descriptive audio is an audio in which descriptions of visual elements of a video are
|
||||
* added in the original audio, with the goal to make a video more accessible to blind and
|
||||
* visually impaired people.
|
||||
* </p>
|
||||
*
|
||||
* @return {@code true} this audio stream is a descriptive audio, {@code false} otherwise
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Audio_description">
|
||||
* https://en.wikipedia.org/wiki/Audio_description</a>
|
||||
*/
|
||||
public boolean isDescriptive() {
|
||||
return isDescriptive;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.extractor.stream;
|
|||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -230,26 +231,7 @@ public final class SubtitlesStream extends Stream {
|
|||
final boolean autoGenerated,
|
||||
@Nullable final String manifestUrl) {
|
||||
super(id, content, isUrl, mediaFormat, deliveryMethod, manifestUrl);
|
||||
|
||||
/*
|
||||
* Locale.forLanguageTag only for Android API >= 21
|
||||
* Locale.Builder only for Android API >= 21
|
||||
* Country codes doesn't work well without
|
||||
*/
|
||||
final String[] splits = languageCode.split("-");
|
||||
switch (splits.length) {
|
||||
case 2:
|
||||
this.locale = new Locale(splits[0], splits[1]);
|
||||
break;
|
||||
case 3:
|
||||
// Complex variants don't work!
|
||||
this.locale = new Locale(splits[0], splits[1], splits[2]);
|
||||
break;
|
||||
default:
|
||||
this.locale = new Locale(splits[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
this.locale = LocaleCompat.forLanguageTag(languageCode);
|
||||
this.code = languageCode;
|
||||
this.format = mediaFormat;
|
||||
this.autoGenerated = autoGenerated;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package org.schabi.newpipe.extractor.utils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class contains a simple implementation of {@link Locale#forLanguageTag(String)} for Android
|
||||
* API levels below 21 (Lollipop). This is needed as core library desugaring does not backport that
|
||||
* method as of this writing.
|
||||
*
|
||||
* Relevant issue: https://issuetracker.google.com/issues/171182330
|
||||
*/
|
||||
public final class LocaleCompat {
|
||||
private LocaleCompat() {
|
||||
}
|
||||
|
||||
// Source: The AndroidX LocaleListCompat class's private forLanguageTagCompat() method.
|
||||
// Use Locale.forLanguageTag() on Android API level >= 21 / Java instead.
|
||||
public static Locale forLanguageTag(final String str) {
|
||||
if (str.contains("-")) {
|
||||
final String[] args = str.split("-", -1);
|
||||
if (args.length > 2) {
|
||||
return new Locale(args[0], args[1], args[2]);
|
||||
} else if (args.length > 1) {
|
||||
return new Locale(args[0], args[1]);
|
||||
} else if (args.length == 1) {
|
||||
return new Locale(args[0]);
|
||||
}
|
||||
} else if (str.contains("_")) {
|
||||
final String[] args = str.split("_", -1);
|
||||
if (args.length > 2) {
|
||||
return new Locale(args[0], args[1], args[2]);
|
||||
} else if (args.length > 1) {
|
||||
return new Locale(args[0], args[1]);
|
||||
} else if (args.length == 1) {
|
||||
return new Locale(args[0]);
|
||||
}
|
||||
} else {
|
||||
return new Locale(str);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Can not parse language tag: [" + str + "]");
|
||||
}
|
||||
}
|
|
@ -306,23 +306,8 @@ public final class Utils {
|
|||
return map == null || map.isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isWhitespace(final int c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r';
|
||||
}
|
||||
|
||||
public static boolean isBlank(final String string) {
|
||||
if (isNullOrEmpty(string)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int length = string.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!isWhitespace(string.codePointAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return string == null || string.isBlank();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -37,6 +37,7 @@ public class BandcampCommentsExtractorTest {
|
|||
@Test
|
||||
public void testGetCommentsAllData() throws IOException, ExtractionException {
|
||||
ListExtractor.InfoItemsPage<CommentsInfoItem> comments = extractor.getInitialPage();
|
||||
assertTrue(comments.hasNextPage());
|
||||
|
||||
DefaultTests.defaultTestListOfItems(Bandcamp, comments.getItems(), comments.getErrors());
|
||||
for (CommentsInfoItem c : comments.getItems()) {
|
||||
|
|
|
@ -32,8 +32,8 @@ public class BandcampCommentsLinkHandlerFactoryTest {
|
|||
assertFalse(linkHandler.acceptUrl("https://bandcamp.com"));
|
||||
assertFalse(linkHandler.acceptUrl("https://zachbenson.bandcamp.com/"));
|
||||
assertFalse(linkHandler.acceptUrl("https://example.com/track/sampletrack"));
|
||||
assertFalse(linkHandler.acceptUrl("http://bandcamP.com/?show=38"));
|
||||
|
||||
assertTrue(linkHandler.acceptUrl("http://bandcamP.com/?show=38"));
|
||||
assertTrue(linkHandler.acceptUrl("https://powertothequeerkids.bandcamp.com/album/power-to-the-queer-kids"));
|
||||
assertTrue(linkHandler.acceptUrl("https://zachbenson.bandcamp.com/track/kitchen"));
|
||||
assertTrue(linkHandler.acceptUrl("http://ZachBenson.Bandcamp.COM/Track/U-I-Tonite/"));
|
||||
|
|
|
@ -7,16 +7,20 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.schabi.newpipe.extractor.ServiceList.MediaCCC;
|
||||
|
||||
/**
|
||||
|
@ -85,7 +89,11 @@ public class MediaCCCStreamExtractorTest {
|
|||
@Test
|
||||
public void testAudioStreams() throws Exception {
|
||||
super.testAudioStreams();
|
||||
assertEquals(2, extractor.getAudioStreams().size());
|
||||
final List<AudioStream> audioStreams = extractor.getAudioStreams();
|
||||
assertEquals(2, audioStreams.size());
|
||||
final Locale expectedLocale = LocaleCompat.forLanguageTag("deu");
|
||||
assertTrue(audioStreams.stream().allMatch(audioStream ->
|
||||
Objects.equals(audioStream.getAudioLocale(), expectedLocale)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +163,11 @@ public class MediaCCCStreamExtractorTest {
|
|||
@Test
|
||||
public void testAudioStreams() throws Exception {
|
||||
super.testAudioStreams();
|
||||
assertEquals(2, extractor.getAudioStreams().size());
|
||||
final List<AudioStream> audioStreams = extractor.getAudioStreams();
|
||||
assertEquals(2, audioStreams.size());
|
||||
final Locale expectedLocale = LocaleCompat.forLanguageTag("eng");
|
||||
assertTrue(audioStreams.stream().allMatch(audioStream ->
|
||||
Objects.equals(audioStream.getAudioLocale(), expectedLocale)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -48,7 +48,7 @@ public class PeertubeCommentsExtractorTest {
|
|||
|
||||
@Test
|
||||
void testGetCommentsFromCommentsInfo() throws IOException, ExtractionException {
|
||||
final String comment = "great video";
|
||||
final String comment = "Thanks for creating such an informative video";
|
||||
|
||||
final CommentsInfo commentsInfo =
|
||||
CommentsInfo.getInfo("https://framatube.org/w/kkGMgK9ZtnKfYAgnEtQxbv");
|
||||
|
@ -69,33 +69,33 @@ public class PeertubeCommentsExtractorTest {
|
|||
|
||||
@Test
|
||||
void testGetCommentsAllData() throws IOException, ExtractionException {
|
||||
InfoItemsPage<CommentsInfoItem> comments = extractor.getInitialPage();
|
||||
for (CommentsInfoItem c : comments.getItems()) {
|
||||
assertFalse(Utils.isBlank(c.getUploaderUrl()));
|
||||
assertFalse(Utils.isBlank(c.getUploaderName()));
|
||||
assertFalse(Utils.isBlank(c.getUploaderAvatarUrl()));
|
||||
assertFalse(Utils.isBlank(c.getCommentId()));
|
||||
assertFalse(Utils.isBlank(c.getCommentText().getContent()));
|
||||
assertFalse(Utils.isBlank(c.getName()));
|
||||
assertFalse(Utils.isBlank(c.getTextualUploadDate()));
|
||||
assertFalse(Utils.isBlank(c.getThumbnailUrl()));
|
||||
assertFalse(Utils.isBlank(c.getUrl()));
|
||||
assertEquals(-1, c.getLikeCount());
|
||||
assertTrue(Utils.isBlank(c.getTextualLikeCount()));
|
||||
}
|
||||
extractor.getInitialPage()
|
||||
.getItems()
|
||||
.forEach(commentsInfoItem -> {
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getUploaderUrl()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getUploaderName()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getUploaderAvatarUrl()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getCommentId()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getCommentText().getContent()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getName()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getTextualUploadDate()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getThumbnailUrl()));
|
||||
assertFalse(Utils.isBlank(commentsInfoItem.getUrl()));
|
||||
assertEquals(-1, commentsInfoItem.getLikeCount());
|
||||
assertTrue(Utils.isBlank(commentsInfoItem.getTextualLikeCount()));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean findInComments(InfoItemsPage<CommentsInfoItem> comments, String comment) {
|
||||
private boolean findInComments(final InfoItemsPage<CommentsInfoItem> comments,
|
||||
final String comment) {
|
||||
return findInComments(comments.getItems(), comment);
|
||||
}
|
||||
|
||||
private boolean findInComments(List<CommentsInfoItem> comments, String comment) {
|
||||
for (CommentsInfoItem c : comments) {
|
||||
if (c.getCommentText().getContent().contains(comment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
private boolean findInComments(final List<CommentsInfoItem> comments,
|
||||
final String comment) {
|
||||
return comments.stream()
|
||||
.anyMatch(commentsInfoItem ->
|
||||
commentsInfoItem.getCommentText().getContent().contains(comment));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,13 +18,30 @@ public class PeertubeSearchQHTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testRegularValues() throws Exception {
|
||||
void testVideoSearch() throws Exception {
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/videos?search=asdf", PeerTube.getSearchQHFactory().fromQuery("asdf").getUrl());
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/videos?search=hans", PeerTube.getSearchQHFactory().fromQuery("hans").getUrl());
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/videos?search=Poifj%26jaijf", PeerTube.getSearchQHFactory().fromQuery("Poifj&jaijf").getUrl());
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/videos?search=G%C3%BCl%C3%BCm", PeerTube.getSearchQHFactory().fromQuery("Gülüm").getUrl());
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/videos?search=%3Fj%24%29H%C2%A7B", PeerTube.getSearchQHFactory().fromQuery("?j$)H§B").getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSepiaVideoSearch() throws Exception {
|
||||
assertEquals("https://sepiasearch.org/api/v1/search/videos?search=%3Fj%24%29H%C2%A7B", PeerTube.getSearchQHFactory().fromQuery("?j$)H§B", singletonList(PeertubeSearchQueryHandlerFactory.SEPIA_VIDEOS), "").getUrl());
|
||||
assertEquals("https://anotherpeertubeindex.com/api/v1/search/videos?search=%3Fj%24%29H%C2%A7B", PeerTube.getSearchQHFactory().fromQuery("?j$)H§B", singletonList(PeertubeSearchQueryHandlerFactory.SEPIA_VIDEOS), "", "https://anotherpeertubeindex.com").getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlaylistSearch() throws Exception {
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/video-playlists?search=asdf", PeerTube.getSearchQHFactory().fromQuery("asdf", singletonList(PeertubeSearchQueryHandlerFactory.PLAYLISTS), "").getUrl());
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/video-playlists?search=hans", PeerTube.getSearchQHFactory().fromQuery("hans", singletonList(PeertubeSearchQueryHandlerFactory.PLAYLISTS), "").getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChannelSearch() throws Exception {
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/video-channels?search=asdf", PeerTube.getSearchQHFactory().fromQuery("asdf", singletonList(PeertubeSearchQueryHandlerFactory.CHANNELS), "").getUrl());
|
||||
assertEquals("https://peertube.mastodon.host/api/v1/search/video-channels?search=hans", PeerTube.getSearchQHFactory().fromQuery("hans", singletonList(PeertubeSearchQueryHandlerFactory.CHANNELS), "").getUrl());
|
||||
|
||||
}
|
||||
}
|
|
@ -22,7 +22,8 @@ import java.util.List;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||
|
||||
public class SoundcloudStreamExtractorTest {
|
||||
|
@ -64,7 +65,7 @@ public class SoundcloudStreamExtractorTest {
|
|||
@Override public long expectedViewCountAtLeast() { return 43000; }
|
||||
@Nullable @Override public String expectedUploadDate() { return "2019-05-16 16:28:45.000"; }
|
||||
@Nullable @Override public String expectedTextualUploadDate() { return "2019-05-16 16:28:45"; }
|
||||
@Override public long expectedLikeCountAtLeast() { return -1; }
|
||||
@Override public long expectedLikeCountAtLeast() { return 600; }
|
||||
@Override public long expectedDislikeCountAtLeast() { return -1; }
|
||||
@Override public boolean expectedHasAudioStreams() { return false; }
|
||||
@Override public boolean expectedHasVideoStreams() { return false; }
|
||||
|
@ -127,7 +128,7 @@ public class SoundcloudStreamExtractorTest {
|
|||
@Override public long expectedViewCountAtLeast() { return 386000; }
|
||||
@Nullable @Override public String expectedUploadDate() { return "2016-11-11 01:16:37.000"; }
|
||||
@Nullable @Override public String expectedTextualUploadDate() { return "2016-11-11 01:16:37"; }
|
||||
@Override public long expectedLikeCountAtLeast() { return -1; }
|
||||
@Override public long expectedLikeCountAtLeast() { return 7350; }
|
||||
@Override public long expectedDislikeCountAtLeast() { return -1; }
|
||||
@Override public boolean expectedHasAudioStreams() { return false; }
|
||||
@Override public boolean expectedHasVideoStreams() { return false; }
|
||||
|
@ -170,7 +171,7 @@ public class SoundcloudStreamExtractorTest {
|
|||
@Override public long expectedViewCountAtLeast() { return 27000; }
|
||||
@Nullable @Override public String expectedUploadDate() { return "2019-03-28 13:36:18.000"; }
|
||||
@Nullable @Override public String expectedTextualUploadDate() { return "2019-03-28 13:36:18"; }
|
||||
@Override public long expectedLikeCountAtLeast() { return -1; }
|
||||
@Override public long expectedLikeCountAtLeast() { return 25; }
|
||||
@Override public long expectedDislikeCountAtLeast() { return -1; }
|
||||
@Override public boolean expectedHasVideoStreams() { return false; }
|
||||
@Override public boolean expectedHasSubtitles() { return false; }
|
||||
|
|
|
@ -89,6 +89,7 @@ public class YoutubeCommentsExtractorTest {
|
|||
@Test
|
||||
public void testGetCommentsAllData() throws IOException, ExtractionException {
|
||||
InfoItemsPage<CommentsInfoItem> comments = extractor.getInitialPage();
|
||||
assertTrue(extractor.getCommentsCount() > 5); // at least 5 comments
|
||||
|
||||
DefaultTests.defaultTestListOfItems(YouTube, comments.getItems(), comments.getErrors());
|
||||
for (CommentsInfoItem c : comments.getItems()) {
|
||||
|
@ -344,6 +345,11 @@ public class YoutubeCommentsExtractorTest {
|
|||
assertNotEquals(UNKNOWN_REPLY_COUNT, firstComment.getReplyCount(), "Could not get the reply count of the first comment");
|
||||
assertGreater(300, firstComment.getReplyCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommentsCount() throws IOException, ExtractionException {
|
||||
assertTrue(extractor.getCommentsCount() > 18800);
|
||||
}
|
||||
}
|
||||
|
||||
public static class FormattingTest {
|
||||
|
|
|
@ -20,6 +20,7 @@ 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;
|
||||
|
@ -191,7 +192,7 @@ class YoutubeDashManifestCreatorsTest {
|
|||
() -> assertMpdElement(document),
|
||||
() -> assertPeriodElement(document),
|
||||
() -> assertAdaptationSetElement(document, itagItem),
|
||||
() -> assertRoleElement(document),
|
||||
() -> assertRoleElement(document, itagItem),
|
||||
() -> assertRepresentationElement(document, itagItem),
|
||||
() -> {
|
||||
if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) {
|
||||
|
@ -220,10 +221,19 @@ class YoutubeDashManifestCreatorsTest {
|
|||
@Nonnull final ItagItem itagItem) {
|
||||
final Element element = assertGetElement(document, ADAPTATION_SET, PERIOD);
|
||||
assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType");
|
||||
|
||||
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
|
||||
final Locale itagAudioLocale = itagItem.getAudioLocale();
|
||||
if (itagAudioLocale != null) {
|
||||
assertAttrEquals(itagAudioLocale.getLanguage(), element, "lang");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertRoleElement(@Nonnull final Document document) {
|
||||
assertGetElement(document, ROLE, ADAPTATION_SET);
|
||||
private void assertRoleElement(@Nonnull final Document document,
|
||||
@Nonnull final ItagItem itagItem) {
|
||||
final Element element = assertGetElement(document, ROLE, ADAPTATION_SET);
|
||||
assertAttrEquals(itagItem.isDescriptiveAudio() ? "alternate" : "main", element, "value");
|
||||
}
|
||||
|
||||
private void assertRepresentationElement(@Nonnull final Document document,
|
||||
|
|
|
@ -23,22 +23,22 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMixPlaylistExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection", "NewClassNamingConvention"})
|
||||
public class YoutubeMixPlaylistExtractorTest {
|
||||
|
||||
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/mix/";
|
||||
private static final Map<String, String> dummyCookie = new HashMap<>();
|
||||
|
||||
private static final Map<String, String> dummyCookie = Map.of(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
private static YoutubeMixPlaylistExtractor extractor;
|
||||
|
||||
public static class Mix {
|
||||
|
@ -50,7 +50,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "mix"));
|
||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||
+ "&list=RD" + VIDEO_ID);
|
||||
|
@ -135,7 +134,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
}
|
||||
|
||||
public static class MixWithIndex {
|
||||
|
||||
private static final String VIDEO_ID = "FAqYW76GLPA";
|
||||
private static final String VIDEO_TITLE = "Mix – ";
|
||||
private static final int INDEX = 7; // YT starts the index with 1...
|
||||
|
@ -146,7 +144,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "mixWithIndex"));
|
||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID_AT_INDEX
|
||||
+ "&list=RD" + VIDEO_ID + "&index=" + INDEX);
|
||||
|
@ -233,7 +230,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "myMix"));
|
||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||
+ "&list=RDMM" + VIDEO_ID);
|
||||
|
@ -316,7 +312,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
}
|
||||
|
||||
public static class Invalid {
|
||||
|
||||
private static final String VIDEO_ID = "QMVCAPd5cwBcg";
|
||||
|
||||
@BeforeAll
|
||||
|
@ -324,7 +319,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "invalid"));
|
||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -348,7 +342,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";
|
||||
|
@ -359,7 +352,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "channelMix"));
|
||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
|
||||
+ "&list=RDCM" + CHANNEL_ID);
|
||||
|
@ -424,7 +416,6 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "genreMix"));
|
||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||
+ "&list=RDGMEMYH9CUrFO7CfLJpaD7UR85w");
|
||||
|
@ -504,4 +495,93 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||
assertEquals(PlaylistInfo.PlaylistType.MIX_GENRE, extractor.getPlaylistType());
|
||||
}
|
||||
}
|
||||
|
||||
public static class Music {
|
||||
private static final String VIDEO_ID = "dQw4w9WgXcQ";
|
||||
private static final String MIX_TITLE = "Mix – Rick Astley - Never Gonna Give You Up (Official Music Video)";
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() throws Exception {
|
||||
YoutubeTestsUtils.ensureStateless();
|
||||
YoutubeParsingHelper.setConsentAccepted(true);
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "musicMix"));
|
||||
extractor = (YoutubeMixPlaylistExtractor)
|
||||
YouTube.getPlaylistExtractor("https://m.youtube.com/watch?v=" + VIDEO_ID
|
||||
+ "&list=RDAMVM" + VIDEO_ID);
|
||||
extractor.fetchPage();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServiceId() {
|
||||
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getName() throws Exception {
|
||||
assertEquals(MIX_TITLE, extractor.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getThumbnailUrl() throws Exception {
|
||||
final String thumbnailUrl = extractor.getThumbnailUrl();
|
||||
assertIsSecureUrl(thumbnailUrl);
|
||||
ExtractorAsserts.assertContains("yt", thumbnailUrl);
|
||||
ExtractorAsserts.assertContains(VIDEO_ID, thumbnailUrl);
|
||||
}
|
||||
|
||||
@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)
|
||||
.value("playlistId", "RD" + VIDEO_ID)
|
||||
.value("params", "OAE%3D")
|
||||
.done())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(new Page(
|
||||
YOUTUBEI_V1_URL + "next?key=" + getKey(), null, null, dummyCookie, body));
|
||||
assertFalse(streams.getItems().isEmpty());
|
||||
assertTrue(streams.hasNextPage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContinuations() throws Exception {
|
||||
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||
final Set<String> urls = new HashSet<>();
|
||||
|
||||
// Should work infinitely, but for testing purposes only 3 times
|
||||
for (int i = 0; i < 3; i++) {
|
||||
assertTrue(streams.hasNextPage());
|
||||
assertFalse(streams.getItems().isEmpty());
|
||||
|
||||
for (final StreamInfoItem item : streams.getItems()) {
|
||||
// TODO Duplicates are appearing
|
||||
// assertFalse(urls.contains(item.getUrl()));
|
||||
urls.add(item.getUrl());
|
||||
}
|
||||
|
||||
streams = extractor.getPage(streams.getNextPage());
|
||||
}
|
||||
assertTrue(streams.hasNextPage());
|
||||
assertFalse(streams.getItems().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStreamCount() throws ParsingException {
|
||||
assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPlaylistType() throws ParsingException {
|
||||
assertEquals(PlaylistInfo.PlaylistType.MIX_MUSIC, extractor.getPlaylistType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,14 +11,14 @@ import org.schabi.newpipe.extractor.exceptions.FoundAdException;
|
|||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Test for {@link YoutubeStreamLinkHandlerFactory}
|
||||
*/
|
||||
@SuppressWarnings("HttpUrlsUsage")
|
||||
public class YoutubeStreamLinkHandlerFactoryTest {
|
||||
private static YoutubeStreamLinkHandlerFactory linkHandler;
|
||||
|
||||
|
@ -53,25 +53,25 @@ public class YoutubeStreamLinkHandlerFactoryTest {
|
|||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"https://www.youtube.com/watch?v=jZViOEv90dI",
|
||||
"https://www.youtube.com/watch?v=jZViOEv90dI&t=100",
|
||||
"https://WWW.YouTube.com/watch?v=jZViOEv90dI&t=100",
|
||||
"HTTPS://www.youtube.com/watch?v=jZViOEv90dI&t=100",
|
||||
"https://youtu.be/jZViOEv90dI?t=9s",
|
||||
"HTTPS://Youtu.be/jZViOEv90dI?t=9s",
|
||||
"https://www.youtube.com/embed/jZViOEv90dI",
|
||||
"https://www.youtube-nocookie.com/embed/jZViOEv90dI",
|
||||
"http://www.youtube.com/watch?v=jZViOEv90dI",
|
||||
"http://youtube.com/watch?v=jZViOEv90dI",
|
||||
"http://youtu.be/jZViOEv90dI?t=9s",
|
||||
"http://www.youtube.com/embed/jZViOEv90dI",
|
||||
"http://www.Youtube.com/embed/jZViOEv90dI",
|
||||
"http://www.youtube-nocookie.com/embed/jZViOEv90dI",
|
||||
"vnd.youtube://www.youtube.com/watch?v=jZViOEv90dI",
|
||||
"vnd.youtube:jZViOEv90dI"
|
||||
"https://www.youtube.com/watch?v=9Dpqou5cI08",
|
||||
"https://www.youtube.com/watch?v=9Dpqou5cI08&t=100",
|
||||
"https://WWW.YouTube.com/watch?v=9Dpqou5cI08&t=100",
|
||||
"HTTPS://www.youtube.com/watch?v=9Dpqou5cI08&t=100",
|
||||
"https://youtu.be/9Dpqou5cI08?t=9s",
|
||||
"HTTPS://Youtu.be/9Dpqou5cI08?t=9s",
|
||||
"https://www.youtube.com/embed/9Dpqou5cI08",
|
||||
"https://www.youtube-nocookie.com/embed/9Dpqou5cI08",
|
||||
"http://www.youtube.com/watch?v=9Dpqou5cI08",
|
||||
"http://youtube.com/watch?v=9Dpqou5cI08",
|
||||
"http://youtu.be/9Dpqou5cI08?t=9s",
|
||||
"http://www.youtube.com/embed/9Dpqou5cI08",
|
||||
"http://www.Youtube.com/embed/9Dpqou5cI08",
|
||||
"http://www.youtube-nocookie.com/embed/9Dpqou5cI08",
|
||||
"vnd.youtube://www.youtube.com/watch?v=9Dpqou5cI08",
|
||||
"vnd.youtube:9Dpqou5cI08"
|
||||
})
|
||||
void getId_jZViOEv90dI_fromYt(final String url) throws Exception {
|
||||
assertEquals("jZViOEv90dI", linkHandler.fromUrl(url).getId());
|
||||
void getId_9Dpqou5cI08_fromYt(final String url) throws Exception {
|
||||
assertEquals("9Dpqou5cI08", linkHandler.fromUrl(url).getId());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
@ -117,27 +117,28 @@ public class YoutubeStreamLinkHandlerFactoryTest {
|
|||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"https://www.youtube.com/watch?v=jZViOEv90dI",
|
||||
"https://www.youtube.com/watch?v=jZViOEv90dI&t=100",
|
||||
"https://WWW.YouTube.com/watch?v=jZViOEv90dI&t=100",
|
||||
"HTTPS://www.youtube.com/watch?v=jZViOEv90dI&t=100",
|
||||
"https://youtu.be/jZViOEv90dI?t=9s",
|
||||
"https://www.youtube.com/embed/jZViOEv90dI",
|
||||
"https://www.youtube-nocookie.com/embed/jZViOEv90dI",
|
||||
"http://www.youtube.com/watch?v=jZViOEv90dI",
|
||||
"http://youtu.be/jZViOEv90dI?t=9s",
|
||||
"http://www.youtube.com/embed/jZViOEv90dI",
|
||||
"http://www.youtube-nocookie.com/embed/jZViOEv90dI",
|
||||
"https://www.youtube.com/watch?v=9Dpqou5cI08",
|
||||
"https://www.youtube.com/watch?v=9Dpqou5cI08&t=100",
|
||||
"https://WWW.YouTube.com/watch?v=9Dpqou5cI08&t=100",
|
||||
"HTTPS://www.youtube.com/watch?v=9Dpqou5cI08&t=100",
|
||||
"https://youtu.be/9Dpqou5cI08?t=9s",
|
||||
"https://www.youtube.com/embed/9Dpqou5cI08",
|
||||
"https://www.youtube-nocookie.com/embed/9Dpqou5cI08",
|
||||
"http://www.youtube.com/watch?v=9Dpqou5cI08",
|
||||
"http://youtu.be/9Dpqou5cI08?t=9s",
|
||||
"http://www.youtube.com/embed/9Dpqou5cI08",
|
||||
"http://www.youtube-nocookie.com/embed/9Dpqou5cI08",
|
||||
"http://www.youtube.com/attribution_link?a=JdfC0C9V6ZI&u=%2Fwatch%3Fv%3DEhxJLojIE_o%26feature%3Dshare",
|
||||
"vnd.youtube://www.youtube.com/watch?v=jZViOEv90dI",
|
||||
"vnd.youtube:jZViOEv90dI",
|
||||
"vnd.youtube.launch:jZViOEv90dI",
|
||||
"vnd.youtube://www.youtube.com/watch?v=9Dpqou5cI08",
|
||||
"vnd.youtube:9Dpqou5cI08",
|
||||
"vnd.youtube.launch:9Dpqou5cI08",
|
||||
"https://music.youtube.com/watch?v=O0EDx9WAelc",
|
||||
"https://www.youtube.com/shorts/IOS2fqxwYbA",
|
||||
"http://www.youtube.com/shorts/IOS2fqxwYbA",
|
||||
"http://www.youtube.com/v/IOS2fqxwYbA",
|
||||
"https://www.youtube.com/w/IOS2fqxwYbA",
|
||||
"https://www.youtube.com/watch/IOS2fqxwYbA"
|
||||
"https://www.youtube.com/watch/IOS2fqxwYbA",
|
||||
"https://www.youtube.com/live/rUxyKA_-grg"
|
||||
})
|
||||
void acceptYtUrl(final String url) throws ParsingException {
|
||||
assertTrue(linkHandler.acceptUrl(url));
|
||||
|
|
|
@ -271,7 +271,7 @@ public class YoutubeSearchExtractorTest {
|
|||
Description.PLAIN_TEXT),
|
||||
Collections.singletonList(
|
||||
new URL("https://www.who.int/emergencies/diseases/novel-coronavirus-2019")),
|
||||
Collections.singletonList("LEARN MORE")
|
||||
Collections.singletonList("Learn more")
|
||||
));
|
||||
}
|
||||
// testMoreRelatedItems is broken because a video has no duration shown
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
package org.schabi.newpipe.extractor.services.youtube.stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -48,6 +49,7 @@ import org.schabi.newpipe.extractor.stream.Description;
|
|||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
@ -56,6 +58,8 @@ import java.net.URL;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class YoutubeStreamExtractorDefaultTest {
|
||||
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/stream/";
|
||||
|
@ -182,10 +186,10 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||
@Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCsTcErHg8oDvUnTzoqsYeNw"; }
|
||||
@Override public long expectedUploaderSubscriberCountAtLeast() { return 18_000_000; }
|
||||
@Override public List<String> expectedDescriptionContains() {
|
||||
return Arrays.asList("https://www.youtube.com/watch?v=X7FLCHVXpsA&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
|
||||
"https://www.youtube.com/watch?v=Lqv6G0pDNnw&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
|
||||
"https://www.youtube.com/watch?v=XxaRBPyrnBU&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
|
||||
"https://www.youtube.com/watch?v=U-9tUEOFKNU&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34");
|
||||
return Arrays.asList("https://www.youtube.com/watch?v=X7FLCHVXpsA&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
|
||||
"https://www.youtube.com/watch?v=Lqv6G0pDNnw&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
|
||||
"https://www.youtube.com/watch?v=XxaRBPyrnBU&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
|
||||
"https://www.youtube.com/watch?v=U-9tUEOFKNU&list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34");
|
||||
}
|
||||
@Override public long expectedLength() { return 434; }
|
||||
@Override public long expectedViewCountAtLeast() { return 21229200; }
|
||||
|
@ -545,24 +549,43 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||
|
||||
@Test
|
||||
void testCheckAudioStreams() throws Exception {
|
||||
assertTrue(extractor.getAudioStreams().size() > 0);
|
||||
final List<AudioStream> audioStreams = extractor.getAudioStreams();
|
||||
assertFalse(audioStreams.isEmpty());
|
||||
|
||||
for (final AudioStream audioStream : extractor.getAudioStreams()) {
|
||||
assertNotNull(audioStream.getAudioTrackName());
|
||||
for (final AudioStream stream : audioStreams) {
|
||||
assertNotNull(stream.getAudioTrackName());
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
extractor.getAudioStreams()
|
||||
.stream()
|
||||
.anyMatch(audioStream -> audioStream.getAudioTrackName().equals("English"))
|
||||
);
|
||||
assertTrue(audioStreams.stream()
|
||||
.anyMatch(audioStream -> "English".equals(audioStream.getAudioTrackName())));
|
||||
|
||||
assertTrue(
|
||||
extractor.getAudioStreams()
|
||||
.stream()
|
||||
.anyMatch(audioStream -> audioStream.getAudioTrackName().equals("Hindi"))
|
||||
);
|
||||
final Locale hindiLocale = LocaleCompat.forLanguageTag("hi");
|
||||
assertTrue(audioStreams.stream()
|
||||
.anyMatch(audioStream ->
|
||||
Objects.equals(audioStream.getAudioLocale(), hindiLocale)));
|
||||
}
|
||||
}
|
||||
|
||||
public static class DescriptiveAudio {
|
||||
private static final String ID = "TjxC-evzxdk";
|
||||
private static final String URL = BASE_URL + ID;
|
||||
private static StreamExtractor extractor;
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() throws Exception {
|
||||
YoutubeTestsUtils.ensureStateless();
|
||||
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "descriptiveAudio"));
|
||||
extractor = YouTube.getStreamExtractor(URL);
|
||||
extractor.fetchPage();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckDescriptiveAudio() throws Exception {
|
||||
assertFalse(extractor.getAudioStreams().isEmpty());
|
||||
|
||||
assertTrue(extractor.getAudioStreams()
|
||||
.stream()
|
||||
.anyMatch(AudioStream::isDescriptive));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,23 +37,20 @@
|
|||
"content-type": [
|
||||
"text/javascript; charset\u003dutf-8"
|
||||
],
|
||||
"critical-ch": [
|
||||
"Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Full-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version"
|
||||
],
|
||||
"cross-origin-opener-policy-report-only": [
|
||||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 12 Aug 2022 17:15:42 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:53 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 12 Aug 2022 17:15:42 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:53 GMT"
|
||||
],
|
||||
"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-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||
],
|
||||
"report-to": [
|
||||
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
|
||||
|
@ -62,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dzvoZRVLWdUQ; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 16-Nov-2019 17:15:42 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+621; expires\u003dSun, 11-Aug-2024 17:15:42 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dKWKE6LaMJTE; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:53 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+649; expires\u003dThu, 21-Nov-2024 10:40:53 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -41,10 +41,10 @@
|
|||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:33:34 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:09 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:33:34 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:09 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||
|
@ -59,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dCpKjiS5i-Jc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 08-Feb-2020 23:33:34 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+185; expires\u003dSun, 03-Nov-2024 23:33:34 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dzHVq5e9PsrU; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:09 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+815; expires\u003dThu, 21-Nov-2024 10:40:09 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,14 +6,17 @@
|
|||
"Origin": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
"Cookie": [
|
||||
"CONSENT\u003dPENDING+488"
|
||||
],
|
||||
"Referer": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Version": [
|
||||
"2.20221104.02.00"
|
||||
"2.20221118.01.00"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
],
|
||||
"Content-Type": [
|
||||
"application/json"
|
||||
|
@ -207,11 +210,11 @@
|
|||
50,
|
||||
49,
|
||||
49,
|
||||
48,
|
||||
52,
|
||||
49,
|
||||
56,
|
||||
46,
|
||||
48,
|
||||
50,
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
48,
|
||||
|
@ -338,20 +341,11 @@
|
|||
"application/json; charset\u003dUTF-8"
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:33:35 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:33:35 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See g.co/p3phelp for more info.\""
|
||||
"Tue, 22 Nov 2022 10:40:10 GMT"
|
||||
],
|
||||
"server": [
|
||||
"scaffolding on HTTPServer2"
|
||||
],
|
||||
"set-cookie": [
|
||||
"CONSENT\u003dPENDING+409; expires\u003dSun, 03-Nov-2024 23:33:34 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"vary": [
|
||||
"Origin",
|
||||
"X-Origin",
|
||||
|
@ -367,7 +361,7 @@
|
|||
"0"
|
||||
]
|
||||
},
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgs0U0t2ZHlyb2h2byjOwpabBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221104.02.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x3b6bf871cd2e8980\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714251,23804281,23882503,23918597,23934970,23946420,23966208,23983296,23986034,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24120819,24135310,24135692,24140247,24152443,24161116,24162920,24164186,24166867,24169501,24181174,24185614,24186126,24187043,24187377,24191629,24199724,24211178,24219713,24224266,24229161,24230619,24231806,24241378,24248091,24254502,24255543,24255545,24256985,24260783,24262346,24263796,24267564,24267570,24268142,24278596,24279196,24281948,24283093,24283556,24287327,24287604,24287795,24288912,24290971,24291857,24292955,24293804,24299747,24390674,24393382,24396550,24396645,24396818,24398125,24401557,24406381,24406605,24406984,24407200,24407665,39322399,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221104\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"23998056,24396818,24007246,24135310,24186126,24283556,24224266,24255543,23934970,24161116,24406605,24140247,24291857,24263796,24267564,24260783,23983296,24229161,24191629,24152443,24398125,24211178,1714251,24287327,24002025,24166867,24262346,24283093,24406381,24241378,24162920,24288912,24169501,24181174,24407200,24034168,24390674,24077241,24287795,24281948,24004644,39322504,24279196,24254502,24255545,23918597,24120819,23986034,39322574,24401557,24393382,24256985,24080738,24002022,39322399,24219713,24293804,24036948,24396550,24135692,24185614,24199724,23882503,24187377,24268142,24396645,24187043,24164186,24287604,24299747,23966208,24267570,24278596,24406984,24231806,24248091,24292955,24407665,23804281,24001373,23946420,24230619,24290971\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIns3-hNiV-wIVaNMRCB2APQYaMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UC6nSFpj9HTCZ5t-N3Rm3-HA\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtBeEFQY2FQVVkzSSiKzvKbBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221118.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x1757a4c0093ffdd1\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714243,23804281,23882502,23918597,23934970,23946420,23966208,23974157,23983296,23986030,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036947,24077241,24080738,24108447,24120820,24135310,24140247,24152443,24161116,24162920,24164186,24166867,24169501,24181174,24187043,24187377,24191629,24199724,24211178,24217229,24219713,24228637,24229161,24236210,24241378,24248092,24254502,24255165,24255543,24255545,24260783,24262346,24263136,24263796,24267564,24268142,24269412,24278596,24279196,24282153,24283093,24283656,24287327,24287603,24288047,24288912,24290971,24291857,24292955,24293803,24298324,24299747,24390674,24390916,24391543,24392059,24392401,24396645,24398988,24401013,24401557,24403200,24406314,24406361,24406605,24407200,24407665,24408833,24413358,24413818,24414074,24414162,24415864,24415866,24416291,24417237,24417274,24417486,24418781,24420756,24421162,24425063,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221118\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24279196,24287603,24392059,24282153,24002022,24283656,23934970,24164186,24187043,39322504,24396645,24268142,24298324,23986030,24211178,24254502,24217229,24278596,24236210,24420756,24292955,24417486,24401013,24255545,24001373,24417237,24161116,1714243,24290971,24191629,24415864,24408833,24407200,24421162,24269412,24283093,23974157,24401557,24199724,24288912,24406361,24002025,24418781,39322574,24036947,24187377,24390674,24403200,24080738,24255165,24108447,24219713,24390916,24166867,24260783,24291857,23918597,24263796,24241378,24267564,24417274,24162920,24248092,24288047,24262346,24135310,24287327,24415866,24263136,24392401,24416291,24425063,24004644,23998056,24077241,24293803,24007246,24406314,24034168,24414074,24228637,24255543,23946420,24413358,24413818,24406605,24414162,24398988,23882502,24299747,23804281,23983296,24140247,24120820,24407665,24391543,23966208,24229161,24152443,24169501,24181174\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMI-P_M3szB-wIVxk_gCh23sgBPMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UC6nSFpj9HTCZ5t-N3Rm3-HA\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?key\u003dAIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8\u0026prettyPrint\u003dfalse"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -41,10 +41,10 @@
|
|||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:35:33 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:04 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:35:33 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:04 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||
|
@ -59,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dxkxwnhsB1uM; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 08-Feb-2020 23:35:33 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+978; expires\u003dSun, 03-Nov-2024 23:35:33 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003diUDF-c9d_Kc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:04 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+907; expires\u003dThu, 21-Nov-2024 10:40:04 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,14 +6,17 @@
|
|||
"Origin": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
"Cookie": [
|
||||
"CONSENT\u003dPENDING+488"
|
||||
],
|
||||
"Referer": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Version": [
|
||||
"2.20221104.02.00"
|
||||
"2.20221118.01.00"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
],
|
||||
"Content-Type": [
|
||||
"application/json"
|
||||
|
@ -207,11 +210,11 @@
|
|||
50,
|
||||
49,
|
||||
49,
|
||||
48,
|
||||
52,
|
||||
49,
|
||||
56,
|
||||
46,
|
||||
48,
|
||||
50,
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
48,
|
||||
|
@ -350,20 +353,11 @@
|
|||
"application/json; charset\u003dUTF-8"
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:35:33 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:35:33 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See g.co/p3phelp for more info.\""
|
||||
"Tue, 22 Nov 2022 10:40:04 GMT"
|
||||
],
|
||||
"server": [
|
||||
"scaffolding on HTTPServer2"
|
||||
],
|
||||
"set-cookie": [
|
||||
"CONSENT\u003dPENDING+182; expires\u003dSun, 03-Nov-2024 23:35:33 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"vary": [
|
||||
"Origin",
|
||||
"X-Origin",
|
||||
|
@ -379,7 +373,7 @@
|
|||
"0"
|
||||
]
|
||||
},
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"Cgs5Zm9EalM5UFBXbyjFw5abBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221104.02.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xdaae2f2b9f5ffb6b\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714246,23703446,23804281,23882503,23885487,23918597,23934970,23940247,23946420,23966208,23983296,23986025,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24120820,24135310,24140247,24152442,24161116,24162920,24164186,24166867,24169501,24181174,24185614,24186126,24187043,24187377,24191629,24199724,24209350,24211178,24219713,24224266,24229161,24230619,24241378,24246503,24248091,24254502,24255543,24255545,24256985,24260783,24262346,24262775,24267564,24267570,24268142,24275719,24278596,24279196,24279540,24282724,24283093,24283495,24283556,24286003,24286017,24286024,24287134,24287327,24287795,24288045,24288912,24290840,24290971,24291857,24292955,24293803,24295894,24297748,24299747,24390675,24390916,24391543,24391709,24392399,24392847,24393382,24396645,24396818,24398048,24398125,24399050,24400009,24401557,24406363,24406381,24406604,24406984,24407199,24407452,24407665,24408532,24409034,24409320,24412600,39322399,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221104\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24287134,24293803,24135310,23918597,23998056,23986025,24396818,24007246,23940247,24255543,24283556,24260783,24080738,24407452,24409320,24219713,23983296,24140247,24209350,24291857,24407199,24286024,24267564,24412600,24391709,24286017,24286003,24287327,24036948,24406363,24398125,24392847,24283093,24187377,23882503,24262346,24290840,24185614,24199724,24002025,24398048,24288912,24390916,24187043,24241378,24164186,24406381,24262775,24282724,24230619,24004644,24287795,24254502,24255545,24186126,24295894,24279196,23934970,24161116,24393382,24401557,24297748,1714246,23885487,24224266,24392399,24399050,24002022,24229161,24409034,24191629,24400009,39322399,39322574,24275719,24288045,24256985,39322504,24279540,24211178,24390675,24166867,24268142,24246503,24396645,23703446,24120820,24408532,24299747,24169501,24406604,24162920,24278596,24267570,24406984,24034168,24391543,23966208,24181174,23946420,23804281,24152442,24001373,24077241,24290971,24248091,24283495,24292955,24407665\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMI35LGvdiV-wIVvNQRCB159ANjMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCEOXxzW2vU0P-0THehuIIeg\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtLZXZHNVZaQlpqWSiEzvKbBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221118.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xd2a82c6ae71596bb\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714249,23703445,23804281,23858057,23882503,23918597,23934970,23946420,23966208,23983296,23986033,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036947,24077241,24080738,24120820,24135310,24140247,24152442,24161116,24162919,24164186,24166867,24169501,24181174,24186126,24187043,24187377,24191629,24198082,24199724,24211178,24217229,24219713,24229161,24241378,24244167,24248092,24254502,24255163,24255543,24255545,24256985,24260783,24262346,24263796,24265425,24267564,24268142,24278596,24279196,24283093,24287327,24288045,24288912,24290971,24291857,24292955,24293803,24296190,24297394,24299357,24299747,24390675,24391543,24392401,24392526,24396645,24399011,24400178,24400185,24401557,24403793,24406314,24406605,24407200,24407665,24408438,24410013,24411127,24413364,24413720,24414162,24415865,24415866,24416238,24416290,24417274,24419033,24420149,24420756,24421162,24425061,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221118\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24291857,24416290,23983296,24219713,23804281,24415865,24140247,24406605,24408438,24255543,24414162,24391543,24400178,24400185,24255163,23998056,24162919,24036947,24187377,24293803,24004644,24007246,24120820,24077241,24407665,23946420,24420149,23966208,24199724,24260783,24263796,24297394,24299357,24417274,24161116,24267564,24187043,24403793,24299747,24217229,24415866,24262346,24265425,24419033,24296190,24241378,24186126,24164186,24248092,23882503,24191629,24416238,24229161,24135310,24392401,24406314,24287327,24413720,24211178,24407200,24034168,23934970,23858057,24002022,24288045,24169501,24410013,23703445,39322574,24288912,24421162,24244167,23986033,24401557,24399011,24292955,24283093,24001373,24152442,24181174,24420756,24278596,24411127,24390675,24290971,24268142,24413364,24254502,24002025,24198082,1714249,24279196,24166867,24255545,39322504,23918597,24425061,24392526,24396645,24080738,24256985\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIxbWK3MzB-wIVhNURCB0_Kg-_MghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCEOXxzW2vU0P-0THehuIIeg\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?key\u003dAIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8\u0026prettyPrint\u003dfalse"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -41,10 +41,10 @@
|
|||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:33:17 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:05 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:33:17 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:05 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||
|
@ -59,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003d4oBMSsx6Bx4; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 08-Feb-2020 23:33:17 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+271; expires\u003dSun, 03-Nov-2024 23:33:17 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dFnptoV5w1o0; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:05 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+048; expires\u003dThu, 21-Nov-2024 10:40:05 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,14 +6,17 @@
|
|||
"Origin": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
"Cookie": [
|
||||
"CONSENT\u003dPENDING+488"
|
||||
],
|
||||
"Referer": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Version": [
|
||||
"2.20221104.02.00"
|
||||
"2.20221118.01.00"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
],
|
||||
"Content-Type": [
|
||||
"application/json"
|
||||
|
@ -207,11 +210,11 @@
|
|||
50,
|
||||
49,
|
||||
49,
|
||||
48,
|
||||
52,
|
||||
49,
|
||||
56,
|
||||
46,
|
||||
48,
|
||||
50,
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
48,
|
||||
|
@ -334,20 +337,11 @@
|
|||
"application/json; charset\u003dUTF-8"
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:33:17 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:33:17 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See g.co/p3phelp for more info.\""
|
||||
"Tue, 22 Nov 2022 10:40:05 GMT"
|
||||
],
|
||||
"server": [
|
||||
"scaffolding on HTTPServer2"
|
||||
],
|
||||
"set-cookie": [
|
||||
"CONSENT\u003dPENDING+296; expires\u003dSun, 03-Nov-2024 23:33:17 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"vary": [
|
||||
"Origin",
|
||||
"X-Origin",
|
||||
|
@ -363,7 +357,7 @@
|
|||
"0"
|
||||
]
|
||||
},
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtKbl9TbVJVNTRSUSi9wpabBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221104.02.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x16271ea0aa04ba1e\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714246,23804281,23882502,23885487,23918597,23934970,23940248,23946420,23966208,23983296,23986029,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24108448,24113426,24120819,24135310,24135692,24140247,24143474,24152442,24161116,24162919,24164186,24166867,24169501,24181174,24184445,24185614,24187043,24187377,24191629,24199724,24211178,24219713,24224266,24228637,24229161,24230619,24241378,24248092,24254502,24255543,24255545,24256985,24260783,24262346,24263796,24267564,24267570,24268142,24278596,24279196,24283093,24283556,24286003,24286017,24287327,24287372,24287795,24287820,24288043,24288912,24290840,24290971,24291857,24292900,24292955,24293803,24295708,24295894,24296352,24297101,24298082,24299747,24299873,24390184,24390675,24391537,24392268,24392848,24393382,24393996,24394395,24396645,24396819,24398125,24398981,24399916,24400011,24400607,24401557,24406381,24406605,24406984,24407199,24407444,24407665,24408534,24409586,24410012,24410764,24413106,24413556,24414637,39322399,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221104\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24297101,24120819,24283093,24392848,24401557,24390675,39322504,24254502,24255545,24288912,24393382,24290840,39322574,24287820,24002025,39322399,24004644,24036948,24230619,24162919,24108448,24287795,24279196,24256985,24135310,24396645,23946420,24268142,23918597,24187043,24007246,24293803,24278596,24187377,24283556,23998056,24185614,24390184,24080738,24184445,24267570,24164186,24286003,24286017,24140247,24407665,24406984,23804281,24407444,24219713,24296352,24292900,24287327,24291857,23966208,24398981,24409586,24199724,23885487,24255543,24406605,24135692,24400607,24287372,24299747,24394395,23983296,24391537,24288043,24143474,24001373,24299873,24191629,24263796,24161116,24290971,24077241,24267564,24298082,24152442,24292955,24228637,24260783,24398125,24407199,24399916,24113426,24413106,24248092,23986029,24400011,1714246,24224266,24406381,24410012,23934970,24295708,24408534,23940248,24396819,24262346,24414637,24295894,24413556,24229161,24169501,24241378,24392268,24410764,24393996,24002022,24166867,24034168,24181174,24211178,23882502\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIxfjp_NeV-wIVotMRCB2tsQW5MghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCYJ61XIK64sp6ZFFS8sctxw\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtLd0xCZnpZRnVsSSiFzvKbBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221118.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x894f99cd60218c47\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714254,23804281,23882502,23918597,23934970,23946420,23966208,23983296,23986024,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24120819,24135310,24140247,24152443,24161116,24162920,24164186,24166867,24169501,24181174,24187043,24187377,24191629,24199724,24211178,24217229,24219713,24229161,24241378,24246502,24248091,24254502,24255165,24255543,24255545,24256987,24260102,24260783,24262346,24263796,24267564,24268142,24278596,24279196,24280761,24283093,24287327,24288418,24288912,24290971,24291857,24292955,24293803,24299747,24390675,24392059,24396645,24401013,24401557,24406314,24406605,24406663,24407200,24407665,24411765,24412844,24414162,24415029,24415866,24416650,24417236,24417274,24420358,24420727,24420756,24421162,24424972,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221118\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24255165,24290971,24169501,24001373,23934970,24181174,24268142,24420756,24288418,24278596,24217229,24292955,24401557,39322504,24406663,24034168,24191629,24407200,24279196,24002022,24412844,24246502,23882502,24211178,24120819,24420727,24254502,24401013,39322574,24390675,24255545,24416650,24036948,24424972,24288912,24392059,24002025,24199724,24396645,24421162,24166867,24411765,24080738,24283093,24162920,24420358,24417236,23804281,24407665,23966208,24417274,24267564,24263796,23998056,24187377,24293803,24077241,24280761,24415866,24406605,23946420,24255543,24004644,24007246,23918597,24291857,24219713,24135310,24248091,24287327,24140247,24161116,24406314,23983296,24414162,24299747,1714254,24262346,24260102,24260783,24152443,24229161,23986024,24415029,24256987,24241378,24187043,24164186\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMI2__C3MzB-wIVIt4RCB11Wgi6MghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCYJ61XIK64sp6ZFFS8sctxw\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?key\u003dAIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8\u0026prettyPrint\u003dfalse"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -41,10 +41,10 @@
|
|||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 04 Nov 2022 23:33:39 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:07 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 04 Nov 2022 23:33:39 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:07 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||
|
@ -59,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dJrCfKXiFN0s; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 08-Feb-2020 23:33:39 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+803; expires\u003dSun, 03-Nov-2024 23:33:39 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dNQGuNvp4W3E; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:07 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+566; expires\u003dThu, 21-Nov-2024 10:40:07 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -41,10 +41,10 @@
|
|||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Wed, 02 Nov 2022 18:09:28 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:10 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Wed, 02 Nov 2022 18:09:28 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:10 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||
|
@ -59,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dWzKJMUonlJc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 06-Feb-2020 18:09:28 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+781; expires\u003dFri, 01-Nov-2024 18:09:28 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dzITFJjB3uZU; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:10 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+083; expires\u003dThu, 21-Nov-2024 10:40:10 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,14 +6,17 @@
|
|||
"Origin": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
"Cookie": [
|
||||
"CONSENT\u003dPENDING+488"
|
||||
],
|
||||
"Referer": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Version": [
|
||||
"2.20221101.00.00"
|
||||
"2.20221118.01.00"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
],
|
||||
"Content-Type": [
|
||||
"application/json"
|
||||
|
@ -207,11 +210,11 @@
|
|||
50,
|
||||
49,
|
||||
49,
|
||||
48,
|
||||
49,
|
||||
56,
|
||||
46,
|
||||
48,
|
||||
48,
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
48,
|
||||
|
@ -339,20 +342,11 @@
|
|||
"application/json; charset\u003dUTF-8"
|
||||
],
|
||||
"date": [
|
||||
"Wed, 02 Nov 2022 18:09:29 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Wed, 02 Nov 2022 18:09:29 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See g.co/p3phelp for more info.\""
|
||||
"Tue, 22 Nov 2022 10:40:10 GMT"
|
||||
],
|
||||
"server": [
|
||||
"scaffolding on HTTPServer2"
|
||||
],
|
||||
"set-cookie": [
|
||||
"CONSENT\u003dPENDING+851; expires\u003dFri, 01-Nov-2024 18:09:29 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"vary": [
|
||||
"Origin",
|
||||
"X-Origin",
|
||||
|
@ -368,7 +362,7 @@
|
|||
"0"
|
||||
]
|
||||
},
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtzNzVnMUhsTVZRdyjZ5IqbBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221101.00.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0x837d7832e173abb2\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714243,23804281,23882502,23918597,23934970,23946420,23966208,23983296,23986016,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24120820,24135310,24140247,24152442,24161116,24162920,24164186,24166867,24169501,24181174,24185614,24186126,24187043,24187377,24191629,24199724,24211178,24218780,24219713,24229161,24241378,24248091,24254502,24255165,24255543,24255545,24260783,24262346,24263796,24265820,24266635,24267564,24267570,24268142,24278596,24279196,24279628,24283093,24283556,24287327,24287795,24288912,24290971,24291857,24292955,24299747,24390674,24393382,24396645,24396818,24398124,24401557,24406381,24406604,24406983,24407200,39322399,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221101\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24229161,24191629,24140247,24287327,24291857,24186126,23983296,24120820,24185614,24262346,24299747,24406381,24241378,24267564,24187043,24164186,24263796,24218780,24260783,24169501,24181174,24152442,24001373,24268142,24290971,24292955,1714243,24390674,24278596,24255165,24279196,39322504,24401557,24211178,23986016,23934970,24034168,24393382,23882502,24161116,39322399,24279628,24002022,24407200,24080738,24406983,24219713,24266635,24254502,24036948,24255545,39322574,24265820,24396645,24283093,24002025,24199724,24288912,24406604,24287795,24162920,24166867,24267570,24398124,23966208,24283556,24248091,23804281,24004644,24077241,24187377,24135310,24255543,24396818,24007246,23998056,23946420,23918597\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMI3omG74uQ-wIVIt4RCB31GgQvMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCeY0bbntWzzVIaj2z3QigXg\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"responseBody": "{\"responseContext\":{\"visitorData\":\"CgtlQ1NVamhzSklwdyiKzvKbBg%3D%3D\",\"serviceTrackingParams\":[{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20221118.01.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"ResolveUrl_rid\",\"value\":\"0xb1565d8a16b10176\"}]},{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714248,23795882,23804281,23882685,23918597,23934970,23946420,23966208,23983296,23986023,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24108447,24120819,24135310,24140247,24152443,24161116,24162920,24164186,24166867,24169501,24181174,24187043,24187377,24191629,24199724,24211178,24217229,24219713,24229161,24241378,24248092,24248955,24254502,24255163,24255543,24255545,24256986,24260783,24262346,24263796,24265426,24267564,24268142,24278596,24279196,24281671,24283093,24287327,24287604,24288043,24288912,24290971,24291857,24292955,24293803,24298326,24299357,24299747,24390675,24391539,24392405,24396645,24398996,24399916,24401557,24403045,24404214,24406313,24406367,24406605,24407200,24407454,24407665,24409253,24412682,24413144,24414162,24415866,24416290,24417274,24417486,24418788,24420358,24420364,24420756,24421162,24424279,24424365,39322504,39322574\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20221118\"},{\"key\":\"client.name\",\"value\":\"WEB\"},{\"key\":\"client.fexp\",\"value\":\"24191629,24417274,24267564,24161116,24404214,23998056,24217229,24415866,24004644,23882685,24413144,24298326,24409253,24293803,24007246,23795882,24248092,24403045,24262346,24135310,1714248,24211178,24241378,24291857,24399916,24406367,24420364,23934970,24287327,24187043,24260783,24164186,24398996,24263796,24152443,24424279,23966208,24080738,24140247,24416290,24407665,24255163,24288043,24406313,23804281,24287604,24187377,23983296,24255543,24414162,24299747,24199724,23946420,24406605,24256986,24248955,24299357,24290971,24001373,24390675,24420756,24254502,24036948,24077241,24255545,24268142,24278596,24292955,39322504,23918597,24417486,24391539,24407454,23986023,24219713,24166867,24162920,24396645,24279196,24002022,24424365,24229161,24108447,24169501,24265426,24392405,24181174,39322574,24120819,24418788,24002025,24283093,24412682,24401557,24288912,24421162,24034168,24281671,24407200,24420358\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"endpoint\":{\"clickTrackingParams\":\"IhMIktn23szB-wIVdNMRCB1gNAQsMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/youtubei/v1/navigation/resolve_url\",\"webPageType\":\"WEB_PAGE_TYPE_CHANNEL\",\"rootVe\":3611,\"apiUrl\":\"/youtubei/v1/browse\"},\"resolveUrlCommandMetadata\":{\"isVanityUrl\":true}},\"browseEndpoint\":{\"browseId\":\"UCeY0bbntWzzVIaj2z3QigXg\",\"params\":\"EgC4AQDyBgQKAjIA\"}}}",
|
||||
"latestUrl": "https://www.youtube.com/youtubei/v1/navigation/resolve_url?key\u003dAIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8\u0026prettyPrint\u003dfalse"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -37,23 +37,20 @@
|
|||
"content-type": [
|
||||
"text/javascript; charset\u003dutf-8"
|
||||
],
|
||||
"critical-ch": [
|
||||
"Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Full-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version"
|
||||
],
|
||||
"cross-origin-opener-policy-report-only": [
|
||||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 12 Aug 2022 17:16:22 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:08 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 12 Aug 2022 17:16:22 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:08 GMT"
|
||||
],
|
||||
"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-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||
],
|
||||
"report-to": [
|
||||
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
|
||||
|
@ -62,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dZzVxGpjPidM; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 16-Nov-2019 17:16:22 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+044; expires\u003dSun, 11-Aug-2024 17:16:22 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dSqETKRNnftE; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:08 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+068; expires\u003dThu, 21-Nov-2024 10:40:08 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -6,14 +6,17 @@
|
|||
"Origin": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
"Cookie": [
|
||||
"CONSENT\u003dPENDING+578"
|
||||
],
|
||||
"Referer": [
|
||||
"https://www.youtube.com"
|
||||
],
|
||||
"X-YouTube-Client-Version": [
|
||||
"2.20220809.02.00"
|
||||
"2.20221118.01.00"
|
||||
],
|
||||
"X-YouTube-Client-Name": [
|
||||
"1"
|
||||
],
|
||||
"Content-Type": [
|
||||
"application/json"
|
||||
|
@ -231,13 +234,13 @@
|
|||
48,
|
||||
50,
|
||||
50,
|
||||
48,
|
||||
49,
|
||||
49,
|
||||
49,
|
||||
56,
|
||||
48,
|
||||
57,
|
||||
46,
|
||||
48,
|
||||
50,
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
48,
|
||||
|
@ -346,7 +349,7 @@
|
|||
"application/json; charset\u003dUTF-8"
|
||||
],
|
||||
"date": [
|
||||
"Fri, 12 Aug 2022 17:16:23 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:08 GMT"
|
||||
],
|
||||
"server": [
|
||||
"scaffolding on HTTPServer2"
|
||||
|
|
|
@ -37,23 +37,20 @@
|
|||
"content-type": [
|
||||
"text/javascript; charset\u003dutf-8"
|
||||
],
|
||||
"critical-ch": [
|
||||
"Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Full-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version"
|
||||
],
|
||||
"cross-origin-opener-policy-report-only": [
|
||||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Fri, 12 Aug 2022 17:16:23 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:08 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Fri, 12 Aug 2022 17:16:23 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:08 GMT"
|
||||
],
|
||||
"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-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
|
||||
],
|
||||
"report-to": [
|
||||
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
|
||||
|
@ -62,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003dac29gHtTp4I; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 16-Nov-2019 17:16:23 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+304; expires\u003dSun, 11-Aug-2024 17:16:23 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003dLUzklSvOALI; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:08 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+404; expires\u003dThu, 21-Nov-2024 10:40:08 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -41,10 +41,10 @@
|
|||
"same-origin; report-to\u003d\"youtube_main\""
|
||||
],
|
||||
"date": [
|
||||
"Wed, 02 Nov 2022 17:41:44 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:09 GMT"
|
||||
],
|
||||
"expires": [
|
||||
"Wed, 02 Nov 2022 17:41:44 GMT"
|
||||
"Tue, 22 Nov 2022 10:40:09 GMT"
|
||||
],
|
||||
"p3p": [
|
||||
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||
|
@ -59,9 +59,9 @@
|
|||
"ESF"
|
||||
],
|
||||
"set-cookie": [
|
||||
"YSC\u003d0xEsk8goB80; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dThu, 06-Feb-2020 17:41:44 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+253; expires\u003dFri, 01-Nov-2024 17:41:44 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
"YSC\u003diWN_EQSBC0c; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dWed, 26-Feb-2020 10:40:09 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||
"CONSENT\u003dPENDING+389; expires\u003dThu, 21-Nov-2024 10:40:09 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age\u003d31536000"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue