Merge pull request #509 from TiA4f8R/soundcloud-improvements
Add new exceptions to be able to display different error messages in apps
This commit is contained in:
commit
7e6f464407
36 changed files with 1239 additions and 131 deletions
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class AgeRestrictedContentException extends ContentNotAvailableException {
|
||||||
|
public AgeRestrictedContentException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgeRestrictedContentException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
package org.schabi.newpipe.extractor.exceptions;
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
public class ContentNotAvailableException extends ParsingException {
|
public class ContentNotAvailableException extends ParsingException {
|
||||||
public ContentNotAvailableException(String message) {
|
public ContentNotAvailableException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentNotAvailableException(String message, Throwable cause) {
|
public ContentNotAvailableException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package org.schabi.newpipe.extractor.exceptions;
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
public class ContentNotSupportedException extends ParsingException {
|
public class ContentNotSupportedException extends ParsingException {
|
||||||
public ContentNotSupportedException(String message) {
|
public ContentNotSupportedException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentNotSupportedException(String message, Throwable cause) {
|
public ContentNotSupportedException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,15 +21,15 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ExtractionException extends Exception {
|
public class ExtractionException extends Exception {
|
||||||
public ExtractionException(String message) {
|
public ExtractionException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtractionException(Throwable cause) {
|
public ExtractionException(final Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtractionException(String message, Throwable cause) {
|
public ExtractionException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,11 +21,11 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class FoundAdException extends ParsingException {
|
public class FoundAdException extends ParsingException {
|
||||||
public FoundAdException(String message) {
|
public FoundAdException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FoundAdException(String message, Throwable cause) {
|
public FoundAdException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class GeographicRestrictionException extends ContentNotAvailableException {
|
||||||
|
public GeographicRestrictionException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeographicRestrictionException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class PaidContentException extends ContentNotAvailableException {
|
||||||
|
public PaidContentException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaidContentException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,11 +22,11 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
|
||||||
public class ParsingException extends ExtractionException {
|
public class ParsingException extends ExtractionException {
|
||||||
public ParsingException(String message) {
|
public ParsingException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParsingException(String message, Throwable cause) {
|
public ParsingException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class PrivateContentException extends ContentNotAvailableException {
|
||||||
|
public PrivateContentException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateContentException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
public class ReCaptchaException extends ExtractionException {
|
public class ReCaptchaException extends ExtractionException {
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
public ReCaptchaException(String message, String url) {
|
public ReCaptchaException(final String message, final String url) {
|
||||||
super(message);
|
super(message);
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class SoundCloudGoPlusContentException extends ContentNotAvailableException {
|
||||||
|
public SoundCloudGoPlusContentException() {
|
||||||
|
super("This track is a SoundCloud Go+ track");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundCloudGoPlusContentException(final Throwable cause) {
|
||||||
|
super("This track is a SoundCloud Go+ track", cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class YoutubeMusicPremiumContentException extends ContentNotAvailableException {
|
||||||
|
public YoutubeMusicPremiumContentException() {
|
||||||
|
super("This video is a YouTube Music Premium video");
|
||||||
|
}
|
||||||
|
|
||||||
|
public YoutubeMusicPremiumContentException(final Throwable cause) {
|
||||||
|
super("This video is a YouTube Music Premium video", cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,9 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
|
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
|
||||||
|
@ -43,6 +45,12 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
String policy = track.getString("policy", EMPTY_STRING);
|
String policy = track.getString("policy", EMPTY_STRING);
|
||||||
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
|
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
|
||||||
|
if (policy.equals("SNIP")) {
|
||||||
|
throw new SoundCloudGoPlusContentException();
|
||||||
|
}
|
||||||
|
if (policy.equals("BLOCK")) {
|
||||||
|
throw new GeographicRestrictionException("This track is not available in user's country");
|
||||||
|
}
|
||||||
throw new ContentNotAvailableException("Content not available: policy " + policy);
|
throw new ContentNotAvailableException("Content not available: policy " + policy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,15 @@ import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
|
@ -71,7 +76,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static class DeobfuscateException extends ParsingException {
|
public static class DeobfuscateException extends ParsingException {
|
||||||
DeobfuscateException(String message, Throwable cause) {
|
DeobfuscateException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +99,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
@Nullable
|
@Nullable
|
||||||
private List<SubtitlesStream> subtitles = null;
|
private List<SubtitlesStream> subtitles = null;
|
||||||
|
|
||||||
public YoutubeStreamExtractor(StreamingService service, LinkHandler linkHandler) {
|
public YoutubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +115,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
|
title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
|
||||||
} catch (ParsingException ignored) {
|
} catch (final ParsingException ignored) {
|
||||||
// age-restricted videos cause a ParsingException here
|
// age-restricted videos cause a ParsingException here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,14 +158,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
TimeAgoParser timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.fromLocalizationCode("en"));
|
TimeAgoParser timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.fromLocalizationCode("en"));
|
||||||
OffsetDateTime parsedTime = timeAgoParser.parse(time).offsetDateTime();
|
OffsetDateTime parsedTime = timeAgoParser.parse(time).offsetDateTime();
|
||||||
return DateTimeFormatter.ISO_LOCAL_DATE.format(parsedTime);
|
return DateTimeFormatter.ISO_LOCAL_DATE.format(parsedTime);
|
||||||
} catch (Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try { // Premiered Feb 21, 2020
|
try { // Premiered Feb 21, 2020
|
||||||
final LocalDate localDate = LocalDate.parse(time,
|
final LocalDate localDate = LocalDate.parse(time,
|
||||||
DateTimeFormatter.ofPattern("MMM dd, yyyy", Locale.ENGLISH));
|
DateTimeFormatter.ofPattern("MMM dd, yyyy", Locale.ENGLISH));
|
||||||
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
|
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
|
||||||
} catch (Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +174,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
LocalDate localDate = LocalDate.parse(getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")),
|
LocalDate localDate = LocalDate.parse(getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")),
|
||||||
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH));
|
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH));
|
||||||
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
|
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
|
||||||
} catch (Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParsingException("Could not get upload date");
|
throw new ParsingException("Could not get upload date");
|
||||||
|
@ -196,7 +201,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
|
String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
|
||||||
|
|
||||||
return fixThumbnailUrl(url);
|
return fixThumbnailUrl(url);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get thumbnail url");
|
throw new ParsingException("Could not get thumbnail url");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +215,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
String description = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("description"), true);
|
String description = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("description"), true);
|
||||||
if (description != null && !description.isEmpty()) return new Description(description, Description.HTML);
|
if (description != null && !description.isEmpty()) return new Description(description, Description.HTML);
|
||||||
} catch (ParsingException ignored) {
|
} catch (final ParsingException ignored) {
|
||||||
// age-restricted videos cause a ParsingException here
|
// age-restricted videos cause a ParsingException here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,7 +265,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
.getObject("videoDetails")
|
.getObject("videoDetails")
|
||||||
.getString("lengthSeconds");
|
.getString("lengthSeconds");
|
||||||
return Long.parseLong(duration);
|
return Long.parseLong(duration);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
try {
|
try {
|
||||||
String durationMs = playerResponse
|
String durationMs = playerResponse
|
||||||
.getObject("streamingData")
|
.getObject("streamingData")
|
||||||
|
@ -268,7 +273,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
.getObject(0)
|
.getObject(0)
|
||||||
.getString("approxDurationMs");
|
.getString("approxDurationMs");
|
||||||
return Math.round(Long.parseLong(durationMs) / 1000f);
|
return Math.round(Long.parseLong(durationMs) / 1000f);
|
||||||
} catch (Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
throw new ParsingException("Could not get duration", e);
|
throw new ParsingException("Could not get duration", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,7 +305,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
|
views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
|
||||||
.getObject("videoViewCountRenderer").getObject("viewCount"));
|
.getObject("videoViewCountRenderer").getObject("viewCount"));
|
||||||
} catch (ParsingException ignored) {
|
} catch (final ParsingException ignored) {
|
||||||
// age-restricted videos cause a ParsingException here
|
// age-restricted videos cause a ParsingException here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +328,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
likesString = getVideoPrimaryInfoRenderer().getObject("sentimentBar")
|
likesString = getVideoPrimaryInfoRenderer().getObject("sentimentBar")
|
||||||
.getObject("sentimentBarRenderer").getString("tooltip").split("/")[0];
|
.getObject("sentimentBarRenderer").getString("tooltip").split("/")[0];
|
||||||
} catch (NullPointerException e) {
|
} catch (final NullPointerException e) {
|
||||||
// if this kicks in our button has no content and therefore ratings must be disabled
|
// if this kicks in our button has no content and therefore ratings must be disabled
|
||||||
if (playerResponse.getObject("videoDetails").getBoolean("allowRatings")) {
|
if (playerResponse.getObject("videoDetails").getBoolean("allowRatings")) {
|
||||||
throw new ParsingException("Ratings are enabled even though the like button is missing", e);
|
throw new ParsingException("Ratings are enabled even though the like button is missing", e);
|
||||||
|
@ -331,9 +336,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return Integer.parseInt(Utils.removeNonDigitCharacters(likesString));
|
return Integer.parseInt(Utils.removeNonDigitCharacters(likesString));
|
||||||
} catch (NumberFormatException nfe) {
|
} catch (final NumberFormatException nfe) {
|
||||||
throw new ParsingException("Could not parse \"" + likesString + "\" as an Integer", nfe);
|
throw new ParsingException("Could not parse \"" + likesString + "\" as an Integer", nfe);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
if (getAgeLimit() == NO_AGE_LIMIT) {
|
if (getAgeLimit() == NO_AGE_LIMIT) {
|
||||||
throw new ParsingException("Could not get like count", e);
|
throw new ParsingException("Could not get like count", e);
|
||||||
}
|
}
|
||||||
|
@ -349,7 +354,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
dislikesString = getVideoPrimaryInfoRenderer().getObject("sentimentBar")
|
dislikesString = getVideoPrimaryInfoRenderer().getObject("sentimentBar")
|
||||||
.getObject("sentimentBarRenderer").getString("tooltip").split("/")[1];
|
.getObject("sentimentBarRenderer").getString("tooltip").split("/")[1];
|
||||||
} catch (NullPointerException e) {
|
} catch (final NullPointerException e) {
|
||||||
// if this kicks in our button has no content and therefore ratings must be disabled
|
// if this kicks in our button has no content and therefore ratings must be disabled
|
||||||
if (playerResponse.getObject("videoDetails").getBoolean("allowRatings")) {
|
if (playerResponse.getObject("videoDetails").getBoolean("allowRatings")) {
|
||||||
throw new ParsingException("Ratings are enabled even though the dislike button is missing", e);
|
throw new ParsingException("Ratings are enabled even though the dislike button is missing", e);
|
||||||
|
@ -357,9 +362,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return Integer.parseInt(Utils.removeNonDigitCharacters(dislikesString));
|
return Integer.parseInt(Utils.removeNonDigitCharacters(dislikesString));
|
||||||
} catch (NumberFormatException nfe) {
|
} catch (final NumberFormatException nfe) {
|
||||||
throw new ParsingException("Could not parse \"" + dislikesString + "\" as an Integer", nfe);
|
throw new ParsingException("Could not parse \"" + dislikesString + "\" as an Integer", nfe);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
if (getAgeLimit() == NO_AGE_LIMIT) {
|
if (getAgeLimit() == NO_AGE_LIMIT) {
|
||||||
throw new ParsingException("Could not get dislike count", e);
|
throw new ParsingException("Could not get dislike count", e);
|
||||||
}
|
}
|
||||||
|
@ -373,16 +378,16 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
assertPageFetched();
|
assertPageFetched();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String uploaderUrl = getUrlFromNavigationEndpoint(getVideoSecondaryInfoRenderer()
|
final String uploaderUrl = getUrlFromNavigationEndpoint(getVideoSecondaryInfoRenderer()
|
||||||
.getObject("owner").getObject("videoOwnerRenderer").getObject("navigationEndpoint"));
|
.getObject("owner").getObject("videoOwnerRenderer").getObject("navigationEndpoint"));
|
||||||
if (!isNullOrEmpty(uploaderUrl)) {
|
if (!isNullOrEmpty(uploaderUrl)) {
|
||||||
return uploaderUrl;
|
return uploaderUrl;
|
||||||
}
|
}
|
||||||
} catch (ParsingException ignored) {
|
} catch (final ParsingException ignored) {
|
||||||
// age-restricted videos cause a ParsingException here
|
// age-restricted videos cause a ParsingException here
|
||||||
}
|
}
|
||||||
|
|
||||||
String uploaderId = playerResponse.getObject("videoDetails").getString("channelId");
|
final String uploaderId = playerResponse.getObject("videoDetails").getString("channelId");
|
||||||
if (!isNullOrEmpty(uploaderId)) {
|
if (!isNullOrEmpty(uploaderId)) {
|
||||||
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + uploaderId);
|
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + uploaderId);
|
||||||
}
|
}
|
||||||
|
@ -400,7 +405,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
uploaderName = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("owner")
|
uploaderName = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("owner")
|
||||||
.getObject("videoOwnerRenderer").getObject("title"));
|
.getObject("videoOwnerRenderer").getObject("title"));
|
||||||
} catch (ParsingException ignored) {
|
} catch (final ParsingException ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNullOrEmpty(uploaderName)) {
|
if (isNullOrEmpty(uploaderName)) {
|
||||||
|
@ -430,7 +435,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
url = getVideoSecondaryInfoRenderer().getObject("owner").getObject("videoOwnerRenderer")
|
url = getVideoSecondaryInfoRenderer().getObject("owner").getObject("videoOwnerRenderer")
|
||||||
.getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
|
.getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
|
||||||
} catch (ParsingException ignored) {
|
} catch (final ParsingException ignored) {
|
||||||
// age-restricted videos cause a ParsingException here
|
// age-restricted videos cause a ParsingException here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,14 +483,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
if (!dashManifestUrl.contains("/signature/")) {
|
if (!dashManifestUrl.contains("/signature/")) {
|
||||||
String obfuscatedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifestUrl);
|
String obfuscatedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifestUrl);
|
||||||
String deobfuscatedSig;
|
final String deobfuscatedSig;
|
||||||
|
|
||||||
deobfuscatedSig = deobfuscateSignature(obfuscatedSig);
|
deobfuscatedSig = deobfuscateSignature(obfuscatedSig);
|
||||||
dashManifestUrl = dashManifestUrl.replace("/s/" + obfuscatedSig, "/signature/" + deobfuscatedSig);
|
dashManifestUrl = dashManifestUrl.replace("/s/" + obfuscatedSig, "/signature/" + deobfuscatedSig);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dashManifestUrl;
|
return dashManifestUrl;
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get dash manifest url", e);
|
throw new ParsingException("Could not get dash manifest url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -497,7 +502,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return playerResponse.getObject("streamingData").getString("hlsManifestUrl");
|
return playerResponse.getObject("streamingData").getString("hlsManifestUrl");
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get hls manifest url", e);
|
throw new ParsingException("Could not get hls manifest url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -505,17 +510,17 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public List<AudioStream> getAudioStreams() throws ExtractionException {
|
public List<AudioStream> getAudioStreams() throws ExtractionException {
|
||||||
assertPageFetched();
|
assertPageFetched();
|
||||||
List<AudioStream> audioStreams = new ArrayList<>();
|
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||||
try {
|
|
||||||
for (Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO).entrySet()) {
|
|
||||||
ItagItem itag = entry.getValue();
|
|
||||||
|
|
||||||
AudioStream audioStream = new AudioStream(entry.getKey(), itag);
|
try {
|
||||||
|
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO).entrySet()) {
|
||||||
|
final ItagItem itag = entry.getValue();
|
||||||
|
final AudioStream audioStream = new AudioStream(entry.getKey(), itag);
|
||||||
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
|
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
|
||||||
audioStreams.add(audioStream);
|
audioStreams.add(audioStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get audio streams", e);
|
throw new ParsingException("Could not get audio streams", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -525,17 +530,17 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public List<VideoStream> getVideoStreams() throws ExtractionException {
|
public List<VideoStream> getVideoStreams() throws ExtractionException {
|
||||||
assertPageFetched();
|
assertPageFetched();
|
||||||
List<VideoStream> videoStreams = new ArrayList<>();
|
final List<VideoStream> videoStreams = new ArrayList<>();
|
||||||
try {
|
|
||||||
for (Map.Entry<String, ItagItem> entry : getItags(FORMATS, ItagItem.ItagType.VIDEO).entrySet()) {
|
|
||||||
ItagItem itag = entry.getValue();
|
|
||||||
|
|
||||||
VideoStream videoStream = new VideoStream(entry.getKey(), false, itag);
|
try {
|
||||||
|
for (final Map.Entry<String, ItagItem> entry : getItags(FORMATS, ItagItem.ItagType.VIDEO).entrySet()) {
|
||||||
|
final ItagItem itag = entry.getValue();
|
||||||
|
final VideoStream videoStream = new VideoStream(entry.getKey(), false, itag);
|
||||||
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
|
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
|
||||||
videoStreams.add(videoStream);
|
videoStreams.add(videoStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get video streams", e);
|
throw new ParsingException("Could not get video streams", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,17 +550,17 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public List<VideoStream> getVideoOnlyStreams() throws ExtractionException {
|
public List<VideoStream> getVideoOnlyStreams() throws ExtractionException {
|
||||||
assertPageFetched();
|
assertPageFetched();
|
||||||
List<VideoStream> videoOnlyStreams = new ArrayList<>();
|
final List<VideoStream> videoOnlyStreams = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
for (Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY).entrySet()) {
|
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY).entrySet()) {
|
||||||
ItagItem itag = entry.getValue();
|
final ItagItem itag = entry.getValue();
|
||||||
|
|
||||||
VideoStream videoStream = new VideoStream(entry.getKey(), true, itag);
|
final VideoStream videoStream = new VideoStream(entry.getKey(), true, itag);
|
||||||
if (!Stream.containSimilarStream(videoStream, videoOnlyStreams)) {
|
if (!Stream.containSimilarStream(videoStream, videoOnlyStreams)) {
|
||||||
videoOnlyStreams.add(videoStream);
|
videoOnlyStreams.add(videoStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get video only streams", e);
|
throw new ParsingException("Could not get video only streams", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,7 +577,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public List<SubtitlesStream> getSubtitles(final MediaFormat format) throws ParsingException {
|
public List<SubtitlesStream> getSubtitles(final MediaFormat format) throws ParsingException {
|
||||||
assertPageFetched();
|
assertPageFetched();
|
||||||
// If the video is age restricted getPlayerConfig will fail
|
// if the video is age restricted getPlayerConfig will fail
|
||||||
if (getAgeLimit() != NO_AGE_LIMIT) {
|
if (getAgeLimit() != NO_AGE_LIMIT) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
@ -630,7 +635,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
.getArray("contents").getObject(0).getObject("compactVideoRenderer");
|
.getArray("contents").getObject(0).getObject("compactVideoRenderer");
|
||||||
|
|
||||||
return new YoutubeStreamInfoItemExtractor(videoInfo, getTimeAgoParser());
|
return new YoutubeStreamInfoItemExtractor(videoInfo, getTimeAgoParser());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get next video", e);
|
throw new ParsingException("Could not get next video", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -663,7 +668,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return collector;
|
return collector;
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get related videos", e);
|
throw new ParsingException("Could not get related videos", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,7 +682,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
return getTextFromObject(initialAjaxJson.getObject(2).getObject("playerResponse")
|
return getTextFromObject(initialAjaxJson.getObject(2).getObject("playerResponse")
|
||||||
.getObject("playabilityStatus").getObject("errorScreen")
|
.getObject("playabilityStatus").getObject("errorScreen")
|
||||||
.getObject("playerErrorMessageRenderer").getObject("reason"));
|
.getObject("playerErrorMessageRenderer").getObject("reason"));
|
||||||
} catch (ParsingException | NullPointerException e) {
|
} catch (final ParsingException | NullPointerException e) {
|
||||||
return null; // no error message
|
return null; // no error message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -713,16 +718,74 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
playerResponse = initialAjaxJson.getObject(2).getObject("playerResponse", null);
|
playerResponse = initialAjaxJson.getObject(2).getObject("playerResponse", null);
|
||||||
|
// Save the playerResponse from the youtube.com website,
|
||||||
|
// because there can be restrictions on the embedded player.
|
||||||
|
// E.g. if a video is age-restricted, the embedded player's playabilityStatus says,
|
||||||
|
// that the video cannot be played outside of YouTube,
|
||||||
|
// but does not show the original message.
|
||||||
|
JsonObject youtubePlayerResponse = playerResponse;
|
||||||
|
|
||||||
if (playerResponse == null || !playerResponse.has("streamingData")) {
|
if (playerResponse == null || !playerResponse.has("streamingData")) {
|
||||||
// try to get player response by fetching video info page
|
// try to get player response by fetching video info page
|
||||||
fetchVideoInfoPage();
|
fetchVideoInfoPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject playabilityStatus = playerResponse.getObject("playabilityStatus");
|
if (playerResponse == null && youtubePlayerResponse == null) {
|
||||||
final String status = playabilityStatus.getString("status");
|
throw new ExtractionException("Could not get playerResponse");
|
||||||
// If status exist, and is not "OK", throw a ContentNotAvailableException with the reason.
|
} else if (youtubePlayerResponse == null) {
|
||||||
if (status != null && !status.toLowerCase().equals("ok")) {
|
youtubePlayerResponse = playerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject playabilityStatus = (playerResponse == null ? youtubePlayerResponse : playerResponse)
|
||||||
|
.getObject("playabilityStatus");
|
||||||
|
String status = playabilityStatus.getString("status");
|
||||||
|
// If status exist, and is not "OK", throw the specific exception based on error message
|
||||||
|
// or a ContentNotAvailableException with the reason text if it's an unknown reason.
|
||||||
|
if (status != null && !status.equalsIgnoreCase("ok")) {
|
||||||
|
playabilityStatus = youtubePlayerResponse.getObject("playabilityStatus");
|
||||||
|
status = playabilityStatus.getString("status");
|
||||||
|
|
||||||
final String reason = playabilityStatus.getString("reason");
|
final String reason = playabilityStatus.getString("reason");
|
||||||
|
|
||||||
|
if (status.equalsIgnoreCase("login_required")) {
|
||||||
|
if (reason == null) {
|
||||||
|
final String message = playabilityStatus.getArray("messages").getString(0);
|
||||||
|
if (message != null && message.equals("This is a private video. Please sign in to verify that you may see it.")) {
|
||||||
|
throw new PrivateContentException("This video is private.");
|
||||||
|
}
|
||||||
|
} else if (reason.equals("Sign in to confirm your age")) {
|
||||||
|
// No streams can be fetched, therefore thrown an AgeRestrictedContentException explicitly.
|
||||||
|
throw new AgeRestrictedContentException("This age-restricted video cannot be watched.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status.equalsIgnoreCase("unplayable")) {
|
||||||
|
if (reason != null) {
|
||||||
|
if (reason.equals("This video is only available to Music Premium members")) {
|
||||||
|
throw new YoutubeMusicPremiumContentException();
|
||||||
|
}
|
||||||
|
if (reason.equals("This video requires payment to watch.")) {
|
||||||
|
throw new PaidContentException("This video is a paid video");
|
||||||
|
}
|
||||||
|
if (reason.equals("Join this channel to get access to members-only content like this video, and other exclusive perks.") ||
|
||||||
|
reason.equals("Join this channel to get access to members-only content like this video and other exclusive perks.")) {
|
||||||
|
throw new PaidContentException("This video is only available for members of the channel of this video");
|
||||||
|
}
|
||||||
|
if (reason.equals("Video unavailable")) {
|
||||||
|
final String detailedErrorMessage = playabilityStatus.getObject("errorScreen")
|
||||||
|
.getObject("playerErrorMessageRenderer")
|
||||||
|
.getObject("subreason")
|
||||||
|
.getArray("runs")
|
||||||
|
.getObject(0)
|
||||||
|
.getString("text");
|
||||||
|
if (detailedErrorMessage != null) {
|
||||||
|
if (detailedErrorMessage.equals("The uploader has not made this video available in your country.")) {
|
||||||
|
throw new GeographicRestrictionException("This video is not available in user's country.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new ContentNotAvailableException("Got error: \"" + reason + "\"");
|
throw new ContentNotAvailableException("Got error: \"" + reason + "\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -736,7 +799,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
playerResponse = JsonParser.object().from(videoInfoPage.get("player_response"));
|
playerResponse = JsonParser.object().from(videoInfoPage.get("player_response"));
|
||||||
} catch (JsonParserException e) {
|
} catch (final JsonParserException e) {
|
||||||
throw new ParsingException(
|
throw new ParsingException(
|
||||||
"Could not parse YouTube player response from video info page", e);
|
"Could not parse YouTube player response from video info page", e);
|
||||||
}
|
}
|
||||||
|
@ -753,12 +816,12 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
final String assetsPattern = "\"assets\":.+?\"js\":\\s*(\"[^\"]+\")";
|
final String assetsPattern = "\"assets\":.+?\"js\":\\s*(\"[^\"]+\")";
|
||||||
playerJsUrl = Parser.matchGroup1(assetsPattern, embedPageContent)
|
playerJsUrl = Parser.matchGroup1(assetsPattern, embedPageContent)
|
||||||
.replace("\\", "").replace("\"", "");
|
.replace("\\", "").replace("\"", "");
|
||||||
} catch (Parser.RegexException ex) {
|
} catch (final Parser.RegexException ex) {
|
||||||
// playerJsUrl is still available in the file, just somewhere else TODO
|
// playerJsUrl is still available in the file, just somewhere else TODO
|
||||||
// it is ok not to find it, see how that's handled in getDeobfuscationCode()
|
// it is ok not to find it, see how that's handled in getDeobfuscationCode()
|
||||||
final Document doc = Jsoup.parse(embedPageContent);
|
final Document doc = Jsoup.parse(embedPageContent);
|
||||||
final Elements elems = doc.select("script").attr("name", "player_ias/base");
|
final Elements elems = doc.select("script").attr("name", "player_ias/base");
|
||||||
for (Element elem : elems) {
|
for (final Element elem : elems) {
|
||||||
if (elem.attr("src").contains("base.js")) {
|
if (elem.attr("src").contains("base.js")) {
|
||||||
playerJsUrl = elem.attr("src");
|
playerJsUrl = elem.attr("src");
|
||||||
break;
|
break;
|
||||||
|
@ -768,19 +831,18 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
// Get embed sts
|
// Get embed sts
|
||||||
return Parser.matchGroup1("\"sts\"\\s*:\\s*(\\d+)", embedPageContent);
|
return Parser.matchGroup1("\"sts\"\\s*:\\s*(\\d+)", embedPageContent);
|
||||||
} catch (Exception i) {
|
} catch (final Exception i) {
|
||||||
// if it fails we simply reply with no sts as then it does not seem to be necessary
|
// if it fails we simply reply with no sts as then it does not seem to be necessary
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private String getDeobfuscationFuncName(final String playerCode) throws DeobfuscateException {
|
private String getDeobfuscationFuncName(final String playerCode) throws DeobfuscateException {
|
||||||
Parser.RegexException exception = null;
|
Parser.RegexException exception = null;
|
||||||
for (final String regex : REGEXES) {
|
for (final String regex : REGEXES) {
|
||||||
try {
|
try {
|
||||||
return Parser.matchGroup1(regex, playerCode);
|
return Parser.matchGroup1(regex, playerCode);
|
||||||
} catch (Parser.RegexException re) {
|
} catch (final Parser.RegexException re) {
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
exception = re;
|
exception = re;
|
||||||
}
|
}
|
||||||
|
@ -812,9 +874,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
"function " + DEOBFUSCATION_FUNC_NAME + "(a){return " + deobfuscationFunctionName + "(a);}";
|
"function " + DEOBFUSCATION_FUNC_NAME + "(a){return " + deobfuscationFunctionName + "(a);}";
|
||||||
|
|
||||||
return helperObject + deobfuscateFunction + callerFunction;
|
return helperObject + deobfuscateFunction + callerFunction;
|
||||||
} catch (IOException ioe) {
|
} catch (final IOException ioe) {
|
||||||
throw new DeobfuscateException("Could not load deobfuscate function", ioe);
|
throw new DeobfuscateException("Could not load deobfuscate function", ioe);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new DeobfuscateException("Could not parse deobfuscate function ", e);
|
throw new DeobfuscateException("Could not parse deobfuscate function ", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -855,7 +917,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
context.evaluateString(scope, deobfuscationCode, "deobfuscationCode", 1, null);
|
context.evaluateString(scope, deobfuscationCode, "deobfuscationCode", 1, null);
|
||||||
final Function deobfuscateFunc = (Function) scope.get(DEOBFUSCATION_FUNC_NAME, scope);
|
final Function deobfuscateFunc = (Function) scope.get(DEOBFUSCATION_FUNC_NAME, scope);
|
||||||
result = deobfuscateFunc.call(context, scope, scope, new Object[]{obfuscatedSig});
|
result = deobfuscateFunc.call(context, scope, scope, new Object[]{obfuscatedSig});
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new DeobfuscateException("Could not get deobfuscate signature", e);
|
throw new DeobfuscateException("Could not get deobfuscate signature", e);
|
||||||
} finally {
|
} finally {
|
||||||
Context.exit();
|
Context.exit();
|
||||||
|
@ -874,7 +936,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
.getObject("results").getObject("results").getArray("contents");
|
.getObject("results").getObject("results").getArray("contents");
|
||||||
JsonObject videoPrimaryInfoRenderer = null;
|
JsonObject videoPrimaryInfoRenderer = null;
|
||||||
|
|
||||||
for (Object content : contents) {
|
for (final Object content : contents) {
|
||||||
if (((JsonObject) content).has("videoPrimaryInfoRenderer")) {
|
if (((JsonObject) content).has("videoPrimaryInfoRenderer")) {
|
||||||
videoPrimaryInfoRenderer = ((JsonObject) content).getObject("videoPrimaryInfoRenderer");
|
videoPrimaryInfoRenderer = ((JsonObject) content).getObject("videoPrimaryInfoRenderer");
|
||||||
break;
|
break;
|
||||||
|
@ -896,7 +958,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
.getObject("results").getObject("results").getArray("contents");
|
.getObject("results").getObject("results").getArray("contents");
|
||||||
JsonObject videoSecondaryInfoRenderer = null;
|
JsonObject videoSecondaryInfoRenderer = null;
|
||||||
|
|
||||||
for (Object content : contents) {
|
for (final Object content : contents) {
|
||||||
if (((JsonObject) content).has("videoSecondaryInfoRenderer")) {
|
if (((JsonObject) content).has("videoSecondaryInfoRenderer")) {
|
||||||
videoSecondaryInfoRenderer = ((JsonObject) content).getObject("videoSecondaryInfoRenderer");
|
videoSecondaryInfoRenderer = ((JsonObject) content).getObject("videoSecondaryInfoRenderer");
|
||||||
break;
|
break;
|
||||||
|
@ -975,7 +1037,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
urlAndItags.put(streamUrl, itagItem);
|
urlAndItags.put(streamUrl, itagItem);
|
||||||
}
|
}
|
||||||
} catch (UnsupportedEncodingException ignored) {
|
} catch (final UnsupportedEncodingException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1032,7 +1094,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
result.trimToSize();
|
result.trimToSize();
|
||||||
return result;
|
return result;
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ExtractionException(e);
|
throw new ExtractionException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
|
||||||
public boolean expectedUploaderVerified() { return false; }
|
public boolean expectedUploaderVerified() { return false; }
|
||||||
public String expectedSubChannelName() { return ""; } // default: there is no subchannel
|
public String expectedSubChannelName() { return ""; } // default: there is no subchannel
|
||||||
public String expectedSubChannelUrl() { return ""; } // default: there is no subchannel
|
public String expectedSubChannelUrl() { return ""; } // default: there is no subchannel
|
||||||
|
public boolean expectedDescriptionIsEmpty() { return false; } // default: description is not empty
|
||||||
public abstract List<String> expectedDescriptionContains(); // e.g. for full links
|
public abstract List<String> expectedDescriptionContains(); // e.g. for full links
|
||||||
public abstract long expectedLength();
|
public abstract long expectedLength();
|
||||||
public long expectedTimestamp() { return 0; } // default: there is no timestamp
|
public long expectedTimestamp() { return 0; } // default: there is no timestamp
|
||||||
|
@ -146,7 +147,12 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
|
||||||
public void testDescription() throws Exception {
|
public void testDescription() throws Exception {
|
||||||
final Description description = extractor().getDescription();
|
final Description description = extractor().getDescription();
|
||||||
assertNotNull(description);
|
assertNotNull(description);
|
||||||
|
|
||||||
|
if (expectedDescriptionIsEmpty()) {
|
||||||
|
assertTrue("description is not empty", description.getContent().isEmpty());
|
||||||
|
} else {
|
||||||
assertFalse("description is empty", description.getContent().isEmpty());
|
assertFalse("description is empty", description.getContent().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
for (final String s : expectedDescriptionContains()) {
|
for (final String s : expectedDescriptionContains()) {
|
||||||
assertThat(description.getContent(), containsString(s));
|
assertThat(description.getContent(), containsString(s));
|
||||||
|
@ -225,7 +231,7 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
|
||||||
defaultTestListOfItems(extractor().getService(), relatedStreams.getItems(),
|
defaultTestListOfItems(extractor().getService(), relatedStreams.getItems(),
|
||||||
relatedStreams.getErrors());
|
relatedStreams.getErrors());
|
||||||
} else {
|
} else {
|
||||||
assertNull(relatedStreams);
|
assertTrue(relatedStreams == null || relatedStreams.getItems().isEmpty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
package org.schabi.newpipe.extractor.services.soundcloud;
|
package org.schabi.newpipe.extractor.services.soundcloud;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.downloader.DownloaderTestImpl;
|
import org.schabi.newpipe.downloader.DownloaderTestImpl;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
|
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -16,10 +22,93 @@ import javax.annotation.Nullable;
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||||
|
|
||||||
public class SoundcloudStreamExtractorTest {
|
public class SoundcloudStreamExtractorTest {
|
||||||
|
private static final String SOUNDCLOUD = "https://soundcloud.com/";
|
||||||
|
|
||||||
|
@Ignore("Ignore until #526 is merged. Throwing the ContentNotSupportedException is wrong and going to be fixed by that PR.")
|
||||||
|
public static class SoundcloudGeoRestrictedTrack extends DefaultStreamExtractorTest {
|
||||||
|
private static final String ID = "one-touch";
|
||||||
|
private static final String UPLOADER = SOUNDCLOUD + "jessglynne";
|
||||||
|
private static final int TIMESTAMP = 0;
|
||||||
|
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
|
||||||
|
private static StreamExtractor extractor;
|
||||||
|
|
||||||
|
@Test(expected = GeographicRestrictionException.class)
|
||||||
|
public void geoRestrictedContent() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = SoundCloud.getStreamExtractor(URL);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public StreamExtractor extractor() { return extractor; }
|
||||||
|
@Override public StreamingService expectedService() { return SoundCloud; }
|
||||||
|
@Override public String expectedName() { return "Jess Glynne & Jax Jones - One Touch"; }
|
||||||
|
@Override public String expectedId() { return "621612588"; }
|
||||||
|
@Override public String expectedUrlContains() { return UPLOADER + "/" + ID; }
|
||||||
|
@Override public String expectedOriginalUrlContains() { return URL; }
|
||||||
|
|
||||||
|
@Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; }
|
||||||
|
@Override public String expectedUploaderName() { return "Jess Glynne"; }
|
||||||
|
@Override public String expectedUploaderUrl() { return UPLOADER; }
|
||||||
|
@Override public boolean expectedUploaderVerified() { return true; }
|
||||||
|
@Override public boolean expectedDescriptionIsEmpty() { return true; }
|
||||||
|
@Override public List<String> expectedDescriptionContains() { return Collections.emptyList(); }
|
||||||
|
@Override public long expectedLength() { return 197; }
|
||||||
|
@Override public long expectedTimestamp() { return TIMESTAMP; }
|
||||||
|
@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 expectedDislikeCountAtLeast() { return -1; }
|
||||||
|
@Override public boolean expectedHasVideoStreams() { return false; }
|
||||||
|
@Override public boolean expectedHasSubtitles() { return false; }
|
||||||
|
@Override public boolean expectedHasFrames() { return false; }
|
||||||
|
@Override public int expectedStreamSegmentsCount() { return 0; }
|
||||||
|
@Override public boolean expectedHasRelatedStreams() { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SoundcloudGoPlusTrack extends DefaultStreamExtractorTest {
|
||||||
|
private static final String ID = "places";
|
||||||
|
private static final String UPLOADER = SOUNDCLOUD + "martinsolveig";
|
||||||
|
private static final int TIMESTAMP = 0;
|
||||||
|
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
|
||||||
|
private static StreamExtractor extractor;
|
||||||
|
|
||||||
|
@Test(expected = SoundCloudGoPlusContentException.class)
|
||||||
|
public void goPlusContent() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = SoundCloud.getStreamExtractor(URL);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public StreamExtractor extractor() { return extractor; }
|
||||||
|
@Override public StreamingService expectedService() { return SoundCloud; }
|
||||||
|
@Override public String expectedName() { return "Places (feat. Ina Wroldsen)"; }
|
||||||
|
@Override public String expectedId() { return "292479564"; }
|
||||||
|
@Override public String expectedUrlContains() { return UPLOADER + "/" + ID; }
|
||||||
|
@Override public String expectedOriginalUrlContains() { return URL; }
|
||||||
|
|
||||||
|
@Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; }
|
||||||
|
@Override public String expectedUploaderName() { return "martinsolveig"; }
|
||||||
|
@Override public String expectedUploaderUrl() { return UPLOADER; }
|
||||||
|
@Override public boolean expectedUploaderVerified() { return true; }
|
||||||
|
@Override public boolean expectedDescriptionIsEmpty() { return true; }
|
||||||
|
@Override public List<String> expectedDescriptionContains() { return Collections.emptyList(); }
|
||||||
|
@Override public long expectedLength() { return 30; }
|
||||||
|
@Override public long expectedTimestamp() { return TIMESTAMP; }
|
||||||
|
@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 expectedDislikeCountAtLeast() { return -1; }
|
||||||
|
@Override public boolean expectedHasVideoStreams() { return false; }
|
||||||
|
@Override public boolean expectedHasSubtitles() { return false; }
|
||||||
|
@Override public boolean expectedHasFrames() { return false; }
|
||||||
|
@Override public int expectedStreamSegmentsCount() { return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
public static class CreativeCommonsPlaysWellWithOthers extends DefaultStreamExtractorTest {
|
public static class CreativeCommonsPlaysWellWithOthers extends DefaultStreamExtractorTest {
|
||||||
private static final String ID = "plays-well-with-others-ep-2-what-do-an-army-of-ants-and-an-online-encyclopedia-have-in-common";
|
private static final String ID = "plays-well-with-others-ep-2-what-do-an-army-of-ants-and-an-online-encyclopedia-have-in-common";
|
||||||
private static final String UPLOADER = "https://soundcloud.com/wearecc";
|
private static final String UPLOADER = SOUNDCLOUD + "wearecc";
|
||||||
private static final int TIMESTAMP = 69;
|
private static final int TIMESTAMP = 69;
|
||||||
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
|
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
|
||||||
private static StreamExtractor extractor;
|
private static StreamExtractor extractor;
|
||||||
|
|
|
@ -8,7 +8,11 @@ import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||||
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
|
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
@ -60,6 +64,13 @@ public class YoutubeStreamExtractorDefaultTest {
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "notAvailable"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "notAvailable"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = GeographicRestrictionException.class)
|
||||||
|
public void geoRestrictedContent() throws Exception {
|
||||||
|
final StreamExtractor extractor =
|
||||||
|
YouTube.getStreamExtractor(BASE_URL + "_PL2HJKxnOM");
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = ContentNotAvailableException.class)
|
@Test(expected = ContentNotAvailableException.class)
|
||||||
public void nonExistentFetch() throws Exception {
|
public void nonExistentFetch() throws Exception {
|
||||||
final StreamExtractor extractor =
|
final StreamExtractor extractor =
|
||||||
|
@ -73,6 +84,27 @@ public class YoutubeStreamExtractorDefaultTest {
|
||||||
YouTube.getStreamExtractor(BASE_URL + "INVALID_ID_INVALID_ID");
|
YouTube.getStreamExtractor(BASE_URL + "INVALID_ID_INVALID_ID");
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = PaidContentException.class)
|
||||||
|
public void paidContent() throws Exception {
|
||||||
|
final StreamExtractor extractor =
|
||||||
|
YouTube.getStreamExtractor(BASE_URL + "ayI2iBwGdxw");
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = PrivateContentException.class)
|
||||||
|
public void privateContent() throws Exception {
|
||||||
|
final StreamExtractor extractor =
|
||||||
|
YouTube.getStreamExtractor(BASE_URL + "8VajtrESJzA");
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = YoutubeMusicPremiumContentException.class)
|
||||||
|
public void youtubeMusicPremiumContent() throws Exception {
|
||||||
|
final StreamExtractor extractor =
|
||||||
|
YouTube.getStreamExtractor(BASE_URL + "sMJ8bRN2dak");
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DescriptionTestPewdiepie extends DefaultStreamExtractorTest {
|
public static class DescriptionTestPewdiepie extends DefaultStreamExtractorTest {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -20,7 +20,7 @@
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"responseCode": 200,
|
"responseCode": 200,
|
||||||
"responseMessage": "",
|
"responseMessage": "OK",
|
||||||
"responseHeaders": {
|
"responseHeaders": {
|
||||||
"alt-svc": [
|
"alt-svc": [
|
||||||
"h3-29\u003d\":443\"; ma\u003d2592000,h3-T051\u003d\":443\"; ma\u003d2592000,h3-Q050\u003d\":443\"; ma\u003d2592000,h3-Q046\u003d\":443\"; ma\u003d2592000,h3-Q043\u003d\":443\"; ma\u003d2592000,quic\u003d\":443\"; ma\u003d2592000; v\u003d\"46,43\""
|
"h3-29\u003d\":443\"; ma\u003d2592000,h3-T051\u003d\":443\"; ma\u003d2592000,h3-Q050\u003d\":443\"; ma\u003d2592000,h3-Q046\u003d\":443\"; ma\u003d2592000,h3-Q043\u003d\":443\"; ma\u003d2592000,quic\u003d\":443\"; ma\u003d2592000; v\u003d\"46,43\""
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
"application/json; charset\u003dutf-8"
|
"application/json; charset\u003dutf-8"
|
||||||
],
|
],
|
||||||
"date": [
|
"date": [
|
||||||
"Sat, 13 Feb 2021 19:13:27 GMT"
|
"Sun, 21 Feb 2021 16:32:21 GMT"
|
||||||
],
|
],
|
||||||
"expires": [
|
"expires": [
|
||||||
"Mon, 01 Jan 1990 00:00:00 GMT"
|
"Mon, 01 Jan 1990 00:00:00 GMT"
|
||||||
|
@ -50,14 +50,17 @@
|
||||||
"ESF"
|
"ESF"
|
||||||
],
|
],
|
||||||
"set-cookie": [
|
"set-cookie": [
|
||||||
"GPS\u003d1; Domain\u003d.youtube.com; Expires\u003dSat, 13-Feb-2021 19:43:27 GMT; Path\u003d/; Secure; HttpOnly",
|
"GPS\u003d1; Domain\u003d.youtube.com; Expires\u003dSun, 21-Feb-2021 17:02:21 GMT; Path\u003d/; Secure; HttpOnly",
|
||||||
"YSC\u003djEoKPLL534o; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
"YSC\u003dmqhSM1eHxP0; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
"VISITOR_INFO1_LIVE\u003d3tVKAqhB50g; Domain\u003d.youtube.com; Expires\u003dThu, 12-Aug-2021 19:13:27 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
"VISITOR_INFO1_LIVE\u003dtBlN1T2LiYc; Domain\u003d.youtube.com; Expires\u003dFri, 20-Aug-2021 16:32:21 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
"CONSENT\u003dPENDING+694; expires\u003dFri, 01-Jan-2038 00:00:00 GMT; path\u003d/; domain\u003d.youtube.com"
|
"CONSENT\u003dPENDING+871; expires\u003dFri, 01-Jan-2038 00:00:00 GMT; path\u003d/; domain\u003d.youtube.com"
|
||||||
],
|
],
|
||||||
"strict-transport-security": [
|
"strict-transport-security": [
|
||||||
"max-age\u003d31536000"
|
"max-age\u003d31536000"
|
||||||
],
|
],
|
||||||
|
"transfer-encoding": [
|
||||||
|
"chunked"
|
||||||
|
],
|
||||||
"x-content-type-options": [
|
"x-content-type-options": [
|
||||||
"nosniff"
|
"nosniff"
|
||||||
],
|
],
|
||||||
|
@ -71,7 +74,7 @@
|
||||||
"0"
|
"0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"responseBody": "[\r\n{\"page\": \"watch\",\"rootVe\": \"3832\"},\r\n{\"page\": \"watch\",\"preconnect\": [\"https:\\/\\/r5---sn-4g5ednly.googlevideo.com\\/generate_204\",\"https:\\/\\/r5---sn-4g5ednly.googlevideo.com\\/generate_204?conn2\"]},\r\n{\"page\": \"watch\",\"playerResponse\": {\"responseContext\":{\"serviceTrackingParams\":[{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"is_viewed_live\",\"value\":\"False\"},{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23744176,23987676,23994373,23912909,23890959,23970529,23839597,23934970,23891344,23974595,1714255,23804281,23946420,23918597,23971936,23858057,23891346,23884386,23969934,23996751,23882502,23968386,23986026,23976578,23996375,24000882,23944779\"}]},{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20200214.04.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetPlayer_rid\",\"value\":\"0xedf6baa3a1796f75\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20210114\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"playabilityStatus\":{\"status\":\"ERROR\",\"reason\":\"Video unavailable\",\"errorScreen\":{\"playerErrorMessageRenderer\":{\"reason\":{\"simpleText\":\"Video unavailable\"},\"thumbnail\":{\"thumbnails\":[{\"url\":\"//s.ytimg.com/yts/img/meh7-vflGevej7.png\",\"width\":140,\"height\":100}]},\"icon\":{\"iconType\":\"ERROR_OUTLINE\"}}},\"contextParams\":\"Q0FBU0FnZ0E\u003d\"},\"trackingParams\":\"CAAQu2kiEwjP37bhyOfuAhUWHeAKHRPGDjM\u003d\"}},\r\n{\"page\": \"watch\",\"response\": {\"responseContext\":{\"webResponseContextExtensionData\":{\"ytConfigData\":{\"visitorData\":\"CgszdFZLQXFoQjUwZyjXyaCBBg%3D%3D\",\"rootVisualElementType\":3832}}}},\"xsrf_token\": \"QUFFLUhqbW5vX0NhQzZZc0JmRkdQRHJLWElKSkZUVDc3UXxBQ3Jtc0tuTnNpZm5haW1ySHkzZjM1THpxYk1OMUpMVWtuWC1Pd0FrbnE5Tm0yTldoYkgzdjdxUXBTVzhNaVRuZ2FFc2d4QngzWFBral8xM3d1UGlmdkxnSXp0ekhMWUJCamtkeEV4eUFlR3lZUWl1b1o5RkpObw\\u003d\\u003d\",\"url\": \"/watch?v\\u003dINVALID_ID_\",\"endpoint\": {\"clickTrackingParams\":\"IhMIn9m14cjn7gIV0ox8Ch2Udg5_MghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/watch?v\u003dINVALID_ID_\",\"webPageType\":\"WEB_PAGE_TYPE_WATCH\",\"rootVe\":3832}},\"watchEndpoint\":{\"videoId\":\"INVALID_ID_\"}}},\r\n{\"page\": \"watch\",\"timing\": {\"info\": {\"st\": 0.0 }}}]\r\n",
|
"responseBody": "[\r\n{\"page\": \"watch\",\"rootVe\": \"3832\"},\r\n{\"page\": \"watch\",\"preconnect\": [\"https:\\/\\/r5---sn-25glen7r.googlevideo.com\\/generate_204\",\"https:\\/\\/r5---sn-25glen7r.googlevideo.com\\/generate_204?conn2\"]},\r\n{\"page\": \"watch\",\"playerResponse\": {\"responseContext\":{\"serviceTrackingParams\":[{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"is_viewed_live\",\"value\":\"False\"},{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"1714259,23981915,23974595,23890959,23744176,23987676,23986020,23974883,23999517,23944779,23992808,23839597,24000882,23996624,23891344,23977071,23994373,23966208,23970529,24006263,23891346,23997047,23741522,23857950,23882502,23991264,23996751,23969934,23884386,9466593,23976578,23848212,23934970,23946420,23973444,23804281,23971936,23877025,23980552,23998149,23994474,23968386,23918597,23999478\"}]},{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20200214.04.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetPlayer_rid\",\"value\":\"0xbe1d80f13a2a71eb\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20210114\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"playabilityStatus\":{\"status\":\"ERROR\",\"reason\":\"Video unavailable\",\"errorScreen\":{\"playerErrorMessageRenderer\":{\"reason\":{\"simpleText\":\"Video unavailable\"},\"thumbnail\":{\"thumbnails\":[{\"url\":\"//s.ytimg.com/yts/img/meh7-vflGevej7.png\",\"width\":140,\"height\":100}]},\"icon\":{\"iconType\":\"ERROR_OUTLINE\"}}},\"contextParams\":\"Q0FBU0FnZ0E\u003d\"},\"trackingParams\":\"CAAQu2kiEwiM2ZfWs_vuAhVMBQYAHSlPDN4\u003d\"}},\r\n{\"page\": \"watch\",\"response\": {\"responseContext\":{\"webResponseContextExtensionData\":{\"ytConfigData\":{\"visitorData\":\"Cgt0QmxOMVQyTGlZYyiVlsqBBg%3D%3D\",\"rootVisualElementType\":3832}}}},\"xsrf_token\": \"QUFFLUhqbFdNMzIzMmFLWGdvTjI3M255R3lOMm1lb2h4d3xBQ3Jtc0ttODlkSUMtS3ZlR1RnRVFTLWVvY3hpVjZ5Z091N1R4aXRxanJkaEQzb1p3SlFUVVNLSi1lZTBzR1VhUkRnTG5WQVk0UkNmTFBqeGFnTG5UN3k5dDA2Zk9kbkgyaXdPbzgtZjVtX190ZE5YdzlWR1lCQQ\\u003d\\u003d\",\"url\": \"/watch?v\\u003dINVALID_ID_\",\"endpoint\": {\"clickTrackingParams\":\"IhMIhvSW1rP77gIVrhvxBR0B-w8mMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/watch?v\u003dINVALID_ID_\",\"webPageType\":\"WEB_PAGE_TYPE_WATCH\",\"rootVe\":3832}},\"watchEndpoint\":{\"videoId\":\"INVALID_ID_\"}}},\r\n{\"page\": \"watch\",\"timing\": {\"info\": {\"st\": 0.0 }}}]\r\n",
|
||||||
"latestUrl": "https://www.youtube.com/watch?v\u003dINVALID_ID_\u0026pbj\u003d1"
|
"latestUrl": "https://www.youtube.com/watch?v\u003dINVALID_ID_\u0026pbj\u003d1"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"httpMethod": "GET",
|
||||||
|
"url": "https://www.youtube.com/watch?v\u003d8VajtrESJzA\u0026pbj\u003d1",
|
||||||
|
"headers": {
|
||||||
|
"Accept-Language": [
|
||||||
|
"en-GB, en;q\u003d0.9"
|
||||||
|
],
|
||||||
|
"X-YouTube-Client-Name": [
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
"X-YouTube-Client-Version": [
|
||||||
|
"2.20200214.04.00"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"languageCode": "en",
|
||||||
|
"countryCode": "GB"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"responseCode": 200,
|
||||||
|
"responseMessage": "OK",
|
||||||
|
"responseHeaders": {
|
||||||
|
"alt-svc": [
|
||||||
|
"h3-29\u003d\":443\"; ma\u003d2592000,h3-T051\u003d\":443\"; ma\u003d2592000,h3-Q050\u003d\":443\"; ma\u003d2592000,h3-Q046\u003d\":443\"; ma\u003d2592000,h3-Q043\u003d\":443\"; ma\u003d2592000,quic\u003d\":443\"; ma\u003d2592000; v\u003d\"46,43\""
|
||||||
|
],
|
||||||
|
"cache-control": [
|
||||||
|
"no-cache, no-store, max-age\u003d0, must-revalidate"
|
||||||
|
],
|
||||||
|
"content-disposition": [
|
||||||
|
"attachment"
|
||||||
|
],
|
||||||
|
"content-type": [
|
||||||
|
"application/json; charset\u003dutf-8"
|
||||||
|
],
|
||||||
|
"date": [
|
||||||
|
"Sun, 21 Feb 2021 16:32:24 GMT"
|
||||||
|
],
|
||||||
|
"expires": [
|
||||||
|
"Mon, 01 Jan 1990 00:00:00 GMT"
|
||||||
|
],
|
||||||
|
"p3p": [
|
||||||
|
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||||
|
],
|
||||||
|
"pragma": [
|
||||||
|
"no-cache"
|
||||||
|
],
|
||||||
|
"server": [
|
||||||
|
"ESF"
|
||||||
|
],
|
||||||
|
"set-cookie": [
|
||||||
|
"GPS\u003d1; Domain\u003d.youtube.com; Expires\u003dSun, 21-Feb-2021 17:02:24 GMT; Path\u003d/; Secure; HttpOnly",
|
||||||
|
"YSC\u003d19P0gGWXP2o; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
|
"VISITOR_INFO1_LIVE\u003da-2fj78bANo; Domain\u003d.youtube.com; Expires\u003dFri, 20-Aug-2021 16:32:24 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
|
"CONSENT\u003dPENDING+569; expires\u003dFri, 01-Jan-2038 00:00:00 GMT; path\u003d/; domain\u003d.youtube.com"
|
||||||
|
],
|
||||||
|
"strict-transport-security": [
|
||||||
|
"max-age\u003d31536000"
|
||||||
|
],
|
||||||
|
"transfer-encoding": [
|
||||||
|
"chunked"
|
||||||
|
],
|
||||||
|
"x-content-type-options": [
|
||||||
|
"nosniff"
|
||||||
|
],
|
||||||
|
"x-frame-options": [
|
||||||
|
"SAMEORIGIN"
|
||||||
|
],
|
||||||
|
"x-spf-response-type": [
|
||||||
|
"multipart"
|
||||||
|
],
|
||||||
|
"x-xss-protection": [
|
||||||
|
"0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"responseBody": "[\r\n{\"page\": \"watch\",\"rootVe\": \"3832\"},\r\n{\"page\": \"watch\",\"preconnect\": [\"https:\\/\\/r3---sn-25ge7nsl.googlevideo.com\\/generate_204\",\"https:\\/\\/r3---sn-25ge7nsl.googlevideo.com\\/generate_204?conn2\"]},\r\n{\"page\": \"watch\",\"playerResponse\": {\"responseContext\":{\"serviceTrackingParams\":[{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"is_viewed_live\",\"value\":\"False\"},{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"24002342,23918597,23986023,23983514,23999478,23744176,23880830,9466593,23934970,23931562,23978419,23891347,23940248,23996625,23974595,23890959,23995022,23991736,23966208,23992809,23970529,23946420,23891344,23804281,23882502,23996751,23884386,23971936,24000882,23944779,23885487,23974884,23994984,23994373,23976578,23998150,23994475,23969934,23880836,23839597,23857949,23968386,23983297,23993730,23987676,1714259\"}]},{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20200214.04.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetPlayer_rid\",\"value\":\"0xbf3eb825a2665b63\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20210114\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"playabilityStatus\":{\"status\":\"LOGIN_REQUIRED\",\"messages\":[\"This is a private video. Please sign in to verify that you may see it.\"],\"errorScreen\":{\"playerErrorMessageRenderer\":{\"subreason\":{\"simpleText\":\"Sign in if you\u0027ve been granted access to this video\"},\"reason\":{\"simpleText\":\"Private video\"},\"proceedButton\":{\"buttonRenderer\":{\"style\":\"STYLE_OVERLAY\",\"size\":\"SIZE_DEFAULT\",\"isDisabled\":false,\"text\":{\"simpleText\":\"Sign in\"},\"navigationEndpoint\":{\"clickTrackingParams\":\"CAEQ8FsiEwjlysbXs_vuAhUVGPEFHZMpD64\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"https://accounts.google.com/ServiceLogin?service\u003dyoutube\\u0026uilel\u003d3\\u0026passive\u003dtrue\\u0026continue\u003dhttps%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26hl%3Den-GB%26next%3D%252Fwatch%253Fv%253D8VajtrESJzA\\u0026hl\u003den-GB\",\"webPageType\":\"WEB_PAGE_TYPE_UNKNOWN\",\"rootVe\":83769}},\"signInEndpoint\":{\"nextEndpoint\":{\"clickTrackingParams\":\"CAEQ8FsiEwjlysbXs_vuAhUVGPEFHZMpD64\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/watch?v\u003d8VajtrESJzA\",\"webPageType\":\"WEB_PAGE_TYPE_UNKNOWN\",\"rootVe\":83769}},\"urlEndpoint\":{\"url\":\"/watch?v\u003d8VajtrESJzA\"}}}},\"trackingParams\":\"CAEQ8FsiEwjlysbXs_vuAhUVGPEFHZMpD64\u003d\"}},\"thumbnail\":{\"thumbnails\":[{\"url\":\"//s.ytimg.com/yts/img/meh7-vflGevej7.png\",\"width\":140,\"height\":100}]},\"icon\":{\"iconType\":\"ERROR_OUTLINE\"}}},\"contextParams\":\"Q0FBU0FnZ0E\u003d\"},\"trackingParams\":\"CAAQu2kiEwjlysbXs_vuAhUVGPEFHZMpD64\u003d\"}},\r\n{\"page\": \"watch\",\"response\": {\"responseContext\":{\"webResponseContextExtensionData\":{\"ytConfigData\":{\"visitorData\":\"CgthLTJmajc4YkFObyiYlsqBBg%3D%3D\",\"rootVisualElementType\":3832}}}},\"xsrf_token\": \"QUFFLUhqbXBCdnJkelFzdnNHaEpnUEZObGNvbk5JSTNJd3xBQ3Jtc0tuSDlkZHJYcExYTkt6TVplTDU1cUQwUUgzUThPOVhPYXVPM2N1bDJmYV9weVNpdDdNVUd4cnNCX1VTU0NXcFRQTGdCWk1aNW5FM25nWVJORGs3S0hUSVdDV3NFcHRpNF91THJpN1N2QWp6VkNqSTdLOA\\u003d\\u003d\",\"url\": \"/watch?v\\u003d8VajtrESJzA\",\"endpoint\": {\"clickTrackingParams\":\"IhMI_6bF17P77gIV1h_xBR134Ae2MghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/watch?v\u003d8VajtrESJzA\",\"webPageType\":\"WEB_PAGE_TYPE_WATCH\",\"rootVe\":3832}},\"watchEndpoint\":{\"videoId\":\"8VajtrESJzA\"}}},\r\n{\"page\": \"watch\",\"timing\": {\"info\": {\"st\": 0.0 }}}]\r\n",
|
||||||
|
"latestUrl": "https://www.youtube.com/watch?v\u003d8VajtrESJzA\u0026pbj\u003d1"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"httpMethod": "GET",
|
||||||
|
"url": "https://www.youtube.com/watch?v\u003ddon-t-exist\u0026pbj\u003d1",
|
||||||
|
"headers": {
|
||||||
|
"Accept-Language": [
|
||||||
|
"en-GB, en;q\u003d0.9"
|
||||||
|
],
|
||||||
|
"X-YouTube-Client-Name": [
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
"X-YouTube-Client-Version": [
|
||||||
|
"2.20200214.04.00"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"languageCode": "en",
|
||||||
|
"countryCode": "GB"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"responseCode": 200,
|
||||||
|
"responseMessage": "OK",
|
||||||
|
"responseHeaders": {
|
||||||
|
"alt-svc": [
|
||||||
|
"h3-29\u003d\":443\"; ma\u003d2592000,h3-T051\u003d\":443\"; ma\u003d2592000,h3-Q050\u003d\":443\"; ma\u003d2592000,h3-Q046\u003d\":443\"; ma\u003d2592000,h3-Q043\u003d\":443\"; ma\u003d2592000,quic\u003d\":443\"; ma\u003d2592000; v\u003d\"46,43\""
|
||||||
|
],
|
||||||
|
"cache-control": [
|
||||||
|
"no-cache, no-store, max-age\u003d0, must-revalidate"
|
||||||
|
],
|
||||||
|
"content-disposition": [
|
||||||
|
"attachment"
|
||||||
|
],
|
||||||
|
"content-type": [
|
||||||
|
"application/json; charset\u003dutf-8"
|
||||||
|
],
|
||||||
|
"date": [
|
||||||
|
"Sun, 21 Feb 2021 16:32:25 GMT"
|
||||||
|
],
|
||||||
|
"expires": [
|
||||||
|
"Mon, 01 Jan 1990 00:00:00 GMT"
|
||||||
|
],
|
||||||
|
"p3p": [
|
||||||
|
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
|
||||||
|
],
|
||||||
|
"pragma": [
|
||||||
|
"no-cache"
|
||||||
|
],
|
||||||
|
"server": [
|
||||||
|
"ESF"
|
||||||
|
],
|
||||||
|
"set-cookie": [
|
||||||
|
"GPS\u003d1; Domain\u003d.youtube.com; Expires\u003dSun, 21-Feb-2021 17:02:25 GMT; Path\u003d/; Secure; HttpOnly",
|
||||||
|
"YSC\u003dZYtZ6I2oRtc; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
|
"VISITOR_INFO1_LIVE\u003dlGs7Hq6Bd-s; Domain\u003d.youtube.com; Expires\u003dFri, 20-Aug-2021 16:32:25 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
|
||||||
|
"CONSENT\u003dPENDING+377; expires\u003dFri, 01-Jan-2038 00:00:00 GMT; path\u003d/; domain\u003d.youtube.com"
|
||||||
|
],
|
||||||
|
"strict-transport-security": [
|
||||||
|
"max-age\u003d31536000"
|
||||||
|
],
|
||||||
|
"transfer-encoding": [
|
||||||
|
"chunked"
|
||||||
|
],
|
||||||
|
"x-content-type-options": [
|
||||||
|
"nosniff"
|
||||||
|
],
|
||||||
|
"x-frame-options": [
|
||||||
|
"SAMEORIGIN"
|
||||||
|
],
|
||||||
|
"x-spf-response-type": [
|
||||||
|
"multipart"
|
||||||
|
],
|
||||||
|
"x-xss-protection": [
|
||||||
|
"0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"responseBody": "[\r\n{\"page\": \"watch\",\"rootVe\": \"3832\"},\r\n{\"page\": \"watch\",\"preconnect\": [\"https:\\/\\/r3---sn-25ge7nzs.googlevideo.com\\/generate_204\",\"https:\\/\\/r3---sn-25ge7nzs.googlevideo.com\\/generate_204?conn2\"]},\r\n{\"page\": \"watch\",\"playerResponse\": {\"responseContext\":{\"serviceTrackingParams\":[{\"service\":\"GFEEDBACK\",\"params\":[{\"key\":\"is_viewed_live\",\"value\":\"False\"},{\"key\":\"logged_in\",\"value\":\"0\"},{\"key\":\"e\",\"value\":\"23987364,23891344,23999475,23987908,23884386,23966208,23970529,23983515,23944779,23891347,23890959,23987676,23974595,23994373,9407157,23744176,24002218,23839597,23999565,24000882,23998150,23992808,1714242,23994368,23735348,23918597,23984880,23968386,23986016,23996751,24306355,23934970,23821390,23804281,23882502,23857950,23969934,23946420,23988063,23994474,23971936,23976578,23991736,23996625\"}]},{\"service\":\"CSI\",\"params\":[{\"key\":\"c\",\"value\":\"WEB\"},{\"key\":\"cver\",\"value\":\"2.20200214.04.00\"},{\"key\":\"yt_li\",\"value\":\"0\"},{\"key\":\"GetPlayer_rid\",\"value\":\"0xa53847d070bf2c39\"}]},{\"service\":\"GUIDED_HELP\",\"params\":[{\"key\":\"logged_in\",\"value\":\"0\"}]},{\"service\":\"ECATCHER\",\"params\":[{\"key\":\"client.version\",\"value\":\"2.20210114\"},{\"key\":\"client.name\",\"value\":\"WEB\"}]}],\"mainAppWebResponseContext\":{\"loggedOut\":true},\"webResponseContextExtensionData\":{\"hasDecorated\":true}},\"playabilityStatus\":{\"status\":\"ERROR\",\"reason\":\"Video unavailable\",\"errorScreen\":{\"playerErrorMessageRenderer\":{\"reason\":{\"simpleText\":\"Video unavailable\"},\"thumbnail\":{\"thumbnails\":[{\"url\":\"//s.ytimg.com/yts/img/meh7-vflGevej7.png\",\"width\":140,\"height\":100}]},\"icon\":{\"iconType\":\"ERROR_OUTLINE\"}}},\"contextParams\":\"Q0FBU0FnZ0E\u003d\"},\"trackingParams\":\"CAAQu2kiEwjdxOPXs_vuAhVbIEwKHbXzD7Y\u003d\"}},\r\n{\"page\": \"watch\",\"response\": {\"responseContext\":{\"webResponseContextExtensionData\":{\"ytConfigData\":{\"visitorData\":\"CgtsR3M3SHE2QmQtcyiZlsqBBg%3D%3D\",\"rootVisualElementType\":3832}}}},\"xsrf_token\": \"QUFFLUhqbTNNcmg1NEltaHZEODVYZmZqNmI4RGpheDVxUXxBQ3Jtc0tuMzBYdHEzM096ZGJ1ZXUtSFpRRWNlTk0xakhJM2xIZHJqSkFNUkpHd2tnRk1WSXVDNFFpeXRiWWJ1NHk3T2xvalFBZzBTQjZETWkxZFotYUpyZkhnNjUtcjIxbVo0aFVxdC1XZmhYblpHRnJKS3VqSQ\\u003d\\u003d\",\"url\": \"/watch?v\\u003ddon-t-exist\",\"endpoint\": {\"clickTrackingParams\":\"IhMIvebi17P77gIVXzTxBR2cKA8lMghleHRlcm5hbA\u003d\u003d\",\"commandMetadata\":{\"webCommandMetadata\":{\"url\":\"/watch?v\u003ddon-t-exist\",\"webPageType\":\"WEB_PAGE_TYPE_WATCH\",\"rootVe\":3832}},\"watchEndpoint\":{\"videoId\":\"don-t-exist\"}}},\r\n{\"page\": \"watch\",\"timing\": {\"info\": {\"st\": 0.0 }}}]\r\n",
|
||||||
|
"latestUrl": "https://www.youtube.com/watch?v\u003ddon-t-exist\u0026pbj\u003d1"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue