Merge branch 'dev' of github.com:TeamNewPipe/NewPipeExtractor into channel-tabs

This commit is contained in:
ThetaDev 2023-03-21 00:45:53 +01:00
commit 9cebcf7ab6
370 changed files with 15499 additions and 11382 deletions

View File

@ -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

View File

@ -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"
}
}

View File

@ -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'
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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.
*

View File

@ -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) {

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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+");

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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));

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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,
};
}
}

View File

@ -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"));
}
}

View File

@ -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");

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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("&amp;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

View File

@ -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);

View File

@ -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 = "";

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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");

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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");

View File

@ -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 {

View File

@ -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);
}

View File

@ -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;

View File

@ -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

View File

@ -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 {

View File

@ -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());

View File

@ -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}
*/

View File

@ -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;

View File

@ -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 + "]");
}
}

View File

@ -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

View File

@ -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()) {

View File

@ -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/"));

View File

@ -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

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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; }

View File

@ -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 {

View File

@ -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,

View File

@ -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());
}
}
}

View File

@ -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));

View File

@ -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

View File

@ -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&amp;list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
"https://www.youtube.com/watch?v=Lqv6G0pDNnw&amp;list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
"https://www.youtube.com/watch?v=XxaRBPyrnBU&amp;list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
"https://www.youtube.com/watch?v=U-9tUEOFKNU&amp;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));
}
}
}

View File

@ -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

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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