mirror of
https://github.com/TeamNewPipe/NewPipeExtractor.git
synced 2025-01-07 10:00:34 +05:30
commit
bb3815d19b
@ -11,7 +11,7 @@ NewPipe Extractor is available at JitPack's Maven repo.
|
|||||||
If you're using Gradle, you could add NewPipe Extractor as a dependency with the following steps:
|
If you're using Gradle, you could add NewPipe Extractor as a dependency with the following steps:
|
||||||
|
|
||||||
1. Add `maven { url 'https://jitpack.io' }` to the `repositories` in your `build.gradle`.
|
1. Add `maven { url 'https://jitpack.io' }` to the `repositories` in your `build.gradle`.
|
||||||
2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.7'`the `dependencies` in your `build.gradle`. Replace `v0.21.7` with the latest release.
|
2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.8'`the `dependencies` in your `build.gradle`. Replace `v0.21.8` with the latest release.
|
||||||
|
|
||||||
**Note:** To use NewPipe Extractor in projects with a `minSdkVersion` below 26, [API desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) is required.
|
**Note:** To use NewPipe Extractor in projects with a `minSdkVersion` below 26, [API desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) is required.
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ allprojects {
|
|||||||
sourceCompatibility = 1.8
|
sourceCompatibility = 1.8
|
||||||
targetCompatibility = 1.8
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
version 'v0.21.7'
|
version 'v0.21.8'
|
||||||
group 'com.github.TeamNewPipe'
|
group 'com.github.TeamNewPipe'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.extractor.comments;
|
|||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
@ -9,9 +10,16 @@ import javax.annotation.Nonnull;
|
|||||||
|
|
||||||
public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem> {
|
public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem> {
|
||||||
|
|
||||||
public CommentsExtractor(StreamingService service, ListLinkHandler uiHandler) {
|
public CommentsExtractor(final StreamingService service, final ListLinkHandler uiHandler) {
|
||||||
super(service, uiHandler);
|
super(service, uiHandler);
|
||||||
// TODO Auto-generated constructor stub
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||||
|
* @return <code>true</code> if the comments are disabled otherwise <code>false</code> (default)
|
||||||
|
*/
|
||||||
|
public boolean isCommentsDisabled() throws ExtractionException {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -13,45 +13,56 @@ import java.io.IOException;
|
|||||||
|
|
||||||
public class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
public class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
||||||
|
|
||||||
private CommentsInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
|
private CommentsInfo(
|
||||||
|
final int serviceId,
|
||||||
|
final ListLinkHandler listUrlIdHandler,
|
||||||
|
final String name) {
|
||||||
super(serviceId, listUrlIdHandler, name);
|
super(serviceId, listUrlIdHandler, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommentsInfo getInfo(String url) throws IOException, ExtractionException {
|
public static CommentsInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommentsInfo getInfo(StreamingService serviceByUrl, String url) throws ExtractionException, IOException {
|
public static CommentsInfo getInfo(final StreamingService serviceByUrl, final String url)
|
||||||
|
throws ExtractionException, IOException {
|
||||||
return getInfo(serviceByUrl.getCommentsExtractor(url));
|
return getInfo(serviceByUrl.getCommentsExtractor(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommentsInfo getInfo(CommentsExtractor commentsExtractor) throws IOException, ExtractionException {
|
public static CommentsInfo getInfo(final CommentsExtractor commentsExtractor)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
// for services which do not have a comments extractor
|
// for services which do not have a comments extractor
|
||||||
if (null == commentsExtractor) {
|
if (commentsExtractor == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsExtractor.fetchPage();
|
commentsExtractor.fetchPage();
|
||||||
String name = commentsExtractor.getName();
|
|
||||||
int serviceId = commentsExtractor.getServiceId();
|
final String name = commentsExtractor.getName();
|
||||||
ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();
|
final int serviceId = commentsExtractor.getServiceId();
|
||||||
CommentsInfo commentsInfo = new CommentsInfo(serviceId, listUrlIdHandler, name);
|
final ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();
|
||||||
|
|
||||||
|
final CommentsInfo commentsInfo = new CommentsInfo(serviceId, listUrlIdHandler, name);
|
||||||
commentsInfo.setCommentsExtractor(commentsExtractor);
|
commentsInfo.setCommentsExtractor(commentsExtractor);
|
||||||
InfoItemsPage<CommentsInfoItem> initialCommentsPage = ExtractorHelper.getItemsPageOrLogError(commentsInfo,
|
final InfoItemsPage<CommentsInfoItem> initialCommentsPage =
|
||||||
commentsExtractor);
|
ExtractorHelper.getItemsPageOrLogError(commentsInfo, commentsExtractor);
|
||||||
|
commentsInfo.setCommentsDisabled(commentsExtractor.isCommentsDisabled());
|
||||||
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
|
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
|
||||||
commentsInfo.setNextPage(initialCommentsPage.getNextPage());
|
commentsInfo.setNextPage(initialCommentsPage.getNextPage());
|
||||||
|
|
||||||
return commentsInfo;
|
return commentsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(CommentsInfo commentsInfo, Page page)
|
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||||
throws ExtractionException, IOException {
|
final CommentsInfo commentsInfo,
|
||||||
|
final Page page) throws ExtractionException, IOException {
|
||||||
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo, page);
|
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(StreamingService service, CommentsInfo commentsInfo,
|
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||||
Page page) throws IOException, ExtractionException {
|
final StreamingService service,
|
||||||
|
final CommentsInfo commentsInfo,
|
||||||
|
final Page page) throws IOException, ExtractionException {
|
||||||
if (null == commentsInfo.getCommentsExtractor()) {
|
if (null == commentsInfo.getCommentsExtractor()) {
|
||||||
commentsInfo.setCommentsExtractor(service.getCommentsExtractor(commentsInfo.getUrl()));
|
commentsInfo.setCommentsExtractor(service.getCommentsExtractor(commentsInfo.getUrl()));
|
||||||
commentsInfo.getCommentsExtractor().fetchPage();
|
commentsInfo.getCommentsExtractor().fetchPage();
|
||||||
@ -60,13 +71,30 @@ public class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private transient CommentsExtractor commentsExtractor;
|
private transient CommentsExtractor commentsExtractor;
|
||||||
|
private boolean commentsDisabled = false;
|
||||||
|
|
||||||
public CommentsExtractor getCommentsExtractor() {
|
public CommentsExtractor getCommentsExtractor() {
|
||||||
return commentsExtractor;
|
return commentsExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCommentsExtractor(CommentsExtractor commentsExtractor) {
|
public void setCommentsExtractor(final CommentsExtractor commentsExtractor) {
|
||||||
this.commentsExtractor = commentsExtractor;
|
this.commentsExtractor = commentsExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||||
|
* @return <code>true</code> if the comments are disabled otherwise <code>false</code> (default)
|
||||||
|
* @see CommentsExtractor#isCommentsDisabled()
|
||||||
|
*/
|
||||||
|
public boolean isCommentsDisabled() {
|
||||||
|
return commentsDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||||
|
* @param commentsDisabled <code>true</code> if the comments are disabled otherwise <code>false</code>
|
||||||
|
*/
|
||||||
|
public void setCommentsDisabled(final boolean commentsDisabled) {
|
||||||
|
this.commentsDisabled = commentsDisabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getHlsUrl() {
|
public String getHlsUrl() {
|
||||||
return "";
|
return json.getArray("streamingPlaylists").getObject(0).getString("playlistUrl");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -227,6 +227,11 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||||||
throw new ParsingException("Could not get video streams", e);
|
throw new ParsingException("Could not get video streams", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getStreamType() == StreamType.LIVE_STREAM) {
|
||||||
|
final String url = getHlsUrl();
|
||||||
|
videoStreams.add(new VideoStream(url, MediaFormat.MPEG_4, "720p"));
|
||||||
|
}
|
||||||
|
|
||||||
return videoStreams;
|
return videoStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +288,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamType getStreamType() {
|
public StreamType getStreamType() {
|
||||||
return StreamType.VIDEO_STREAM;
|
return json.getBoolean("isLive") ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -82,7 +82,7 @@ public class PeertubeStreamInfoItemExtractor implements StreamInfoItemExtractor
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamType getStreamType() {
|
public StreamType getStreamType() {
|
||||||
return StreamType.VIDEO_STREAM;
|
return item.getBoolean("isLive") ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -46,13 +46,12 @@ public class ItagItem {
|
|||||||
/// VIDEO ONLY ////////////////////////////////////////////
|
/// VIDEO ONLY ////////////////////////////////////////////
|
||||||
// ID Type Format Resolution FPS ///
|
// ID Type Format Resolution FPS ///
|
||||||
/////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////
|
||||||
// Don't add VideoOnly streams that have normal variants
|
|
||||||
new ItagItem(160, VIDEO_ONLY, MPEG_4, "144p"),
|
new ItagItem(160, VIDEO_ONLY, MPEG_4, "144p"),
|
||||||
new ItagItem(133, VIDEO_ONLY, MPEG_4, "240p"),
|
new ItagItem(133, VIDEO_ONLY, MPEG_4, "240p"),
|
||||||
// new ItagItem(134, VIDEO_ONLY, MPEG_4, "360p"),
|
new ItagItem(134, VIDEO_ONLY, MPEG_4, "360p"),
|
||||||
new ItagItem(135, VIDEO_ONLY, MPEG_4, "480p"),
|
new ItagItem(135, VIDEO_ONLY, MPEG_4, "480p"),
|
||||||
new ItagItem(212, VIDEO_ONLY, MPEG_4, "480p"),
|
new ItagItem(212, VIDEO_ONLY, MPEG_4, "480p"),
|
||||||
// new ItagItem(136, VIDEO_ONLY, MPEG_4, "720p"),
|
new ItagItem(136, VIDEO_ONLY, MPEG_4, "720p"),
|
||||||
new ItagItem(298, VIDEO_ONLY, MPEG_4, "720p60", 60),
|
new ItagItem(298, VIDEO_ONLY, MPEG_4, "720p60", 60),
|
||||||
new ItagItem(137, VIDEO_ONLY, MPEG_4, "1080p"),
|
new ItagItem(137, VIDEO_ONLY, MPEG_4, "1080p"),
|
||||||
new ItagItem(299, VIDEO_ONLY, MPEG_4, "1080p60", 60),
|
new ItagItem(299, VIDEO_ONLY, MPEG_4, "1080p60", 60),
|
||||||
@ -75,6 +74,7 @@ public class ItagItem {
|
|||||||
new ItagItem(313, VIDEO_ONLY, WEBM, "2160p"),
|
new ItagItem(313, VIDEO_ONLY, WEBM, "2160p"),
|
||||||
new ItagItem(315, VIDEO_ONLY, WEBM, "2160p60", 60)
|
new ItagItem(315, VIDEO_ONLY, WEBM, "2160p60", 60)
|
||||||
};
|
};
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -60,6 +60,14 @@ public class YoutubeJavaScriptExtractor {
|
|||||||
return extractJavaScriptCode("d4IGg5dqeO8");
|
return extractJavaScriptCode("d4IGg5dqeO8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the JavaScript code. It will be fetched again the next time
|
||||||
|
* {@link #extractJavaScriptCode()} or {@link #extractJavaScriptCode(String)} is called.
|
||||||
|
*/
|
||||||
|
public static void resetJavaScriptCode() {
|
||||||
|
cachedJavaScriptCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String extractJavaScriptUrl(final String videoId) throws ParsingException {
|
private static String extractJavaScriptUrl(final String videoId) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
final String embedUrl = "https://www.youtube.com/embed/" + videoId;
|
final String embedUrl = "https://www.youtube.com/embed/" + videoId;
|
||||||
|
@ -11,6 +11,7 @@ import org.schabi.newpipe.extractor.MetaInfo;
|
|||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.downloader.Response;
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
import org.schabi.newpipe.extractor.exceptions.*;
|
import org.schabi.newpipe.extractor.exceptions.*;
|
||||||
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
@ -37,7 +38,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.HTTPS;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.join;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 02.03.16.
|
* Created by Christian Schabesberger on 02.03.16.
|
||||||
@ -64,13 +64,22 @@ public class YoutubeParsingHelper {
|
|||||||
private YoutubeParsingHelper() {
|
private YoutubeParsingHelper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String HARDCODED_CLIENT_VERSION = "2.20200214.04.00";
|
public static final String YOUTUBEI_V1_URL = "https://www.youtube.com/youtubei/v1/";
|
||||||
private static String clientVersion;
|
|
||||||
|
|
||||||
|
private static final String HARDCODED_CLIENT_VERSION = "2.20210728.00.00";
|
||||||
|
private static final String HARDCODED_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
|
||||||
|
private static final String MOBILE_YOUTUBE_KEY = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w";
|
||||||
|
private static final String MOBILE_YOUTUBE_CLIENT_VERSION = "16.29.38";
|
||||||
|
private static String clientVersion;
|
||||||
private static String key;
|
private static String key;
|
||||||
|
|
||||||
private static final String[] HARDCODED_YOUTUBE_MUSIC_KEYS = {"AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", "67", "0.1"};
|
private static final String[] HARDCODED_YOUTUBE_MUSIC_KEY =
|
||||||
private static String[] youtubeMusicKeys;
|
{"AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", "67", "1.20210726.00.01"};
|
||||||
|
private static String[] youtubeMusicKey;
|
||||||
|
|
||||||
|
private static boolean keyAndVersionExtracted = false;
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
private static Optional<Boolean> hardcodedClientVersionAndKeyValid = Optional.empty();
|
||||||
|
|
||||||
private static Random numberGenerator = new Random();
|
private static Random numberGenerator = new Random();
|
||||||
|
|
||||||
@ -85,7 +94,8 @@ public class YoutubeParsingHelper {
|
|||||||
*/
|
*/
|
||||||
private static final String CONSENT_COOKIE = "CONSENT=" + CONSENT_COOKIE_VALUE;
|
private static final String CONSENT_COOKIE = "CONSENT=" + CONSENT_COOKIE_VALUE;
|
||||||
|
|
||||||
private static final String FEED_BASE_CHANNEL_ID = "https://www.youtube.com/feeds/videos.xml?channel_id=";
|
private static final String FEED_BASE_CHANNEL_ID =
|
||||||
|
"https://www.youtube.com/feeds/videos.xml?channel_id=";
|
||||||
private static final String FEED_BASE_USER = "https://www.youtube.com/feeds/videos.xml?user=";
|
private static final String FEED_BASE_USER = "https://www.youtube.com/feeds/videos.xml?user=";
|
||||||
|
|
||||||
private static boolean isGoogleURL(String url) {
|
private static boolean isGoogleURL(String url) {
|
||||||
@ -93,30 +103,34 @@ public class YoutubeParsingHelper {
|
|||||||
try {
|
try {
|
||||||
final URL u = new URL(url);
|
final URL u = new URL(url);
|
||||||
final String host = u.getHost();
|
final String host = u.getHost();
|
||||||
return host.startsWith("google.") || host.startsWith("m.google.")
|
return host.startsWith("google.")
|
||||||
|
|| host.startsWith("m.google.")
|
||||||
|| host.startsWith("www.google.");
|
|| host.startsWith("www.google.");
|
||||||
} catch (MalformedURLException e) {
|
} catch (final MalformedURLException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isYoutubeURL(final URL url) {
|
public static boolean isYoutubeURL(@Nonnull final URL url) {
|
||||||
final String host = url.getHost();
|
final String host = url.getHost();
|
||||||
return host.equalsIgnoreCase("youtube.com") || host.equalsIgnoreCase("www.youtube.com")
|
return host.equalsIgnoreCase("youtube.com")
|
||||||
|| host.equalsIgnoreCase("m.youtube.com") || host.equalsIgnoreCase("music.youtube.com");
|
|| host.equalsIgnoreCase("www.youtube.com")
|
||||||
|
|| host.equalsIgnoreCase("m.youtube.com")
|
||||||
|
|| host.equalsIgnoreCase("music.youtube.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isYoutubeServiceURL(final URL url) {
|
public static boolean isYoutubeServiceURL(@Nonnull final URL url) {
|
||||||
final String host = url.getHost();
|
final String host = url.getHost();
|
||||||
return host.equalsIgnoreCase("www.youtube-nocookie.com") || host.equalsIgnoreCase("youtu.be");
|
return host.equalsIgnoreCase("www.youtube-nocookie.com")
|
||||||
|
|| host.equalsIgnoreCase("youtu.be");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isHooktubeURL(final URL url) {
|
public static boolean isHooktubeURL(@Nonnull final URL url) {
|
||||||
final String host = url.getHost();
|
final String host = url.getHost();
|
||||||
return host.equalsIgnoreCase("hooktube.com");
|
return host.equalsIgnoreCase("hooktube.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isInvidioURL(final URL url) {
|
public static boolean isInvidioURL(@Nonnull final URL url) {
|
||||||
final String host = url.getHost();
|
final String host = url.getHost();
|
||||||
return host.equalsIgnoreCase("invidio.us")
|
return host.equalsIgnoreCase("invidio.us")
|
||||||
|| host.equalsIgnoreCase("dev.invidio.us")
|
|| host.equalsIgnoreCase("dev.invidio.us")
|
||||||
@ -153,7 +167,7 @@ public class YoutubeParsingHelper {
|
|||||||
* @return the duration in seconds
|
* @return the duration in seconds
|
||||||
* @throws ParsingException when more than 3 separators are found
|
* @throws ParsingException when more than 3 separators are found
|
||||||
*/
|
*/
|
||||||
public static int parseDurationString(final String input)
|
public static int parseDurationString(@Nonnull final String input)
|
||||||
throws ParsingException, NumberFormatException {
|
throws ParsingException, NumberFormatException {
|
||||||
// If time separator : is not detected, try . instead
|
// If time separator : is not detected, try . instead
|
||||||
final String[] splitInput = input.contains(":")
|
final String[] splitInput = input.contains(":")
|
||||||
@ -194,7 +208,8 @@ public class YoutubeParsingHelper {
|
|||||||
+ Integer.parseInt(Utils.removeNonDigitCharacters(seconds));
|
+ Integer.parseInt(Utils.removeNonDigitCharacters(seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFeedUrlFrom(final String channelIdOrUser) {
|
@Nonnull
|
||||||
|
public static String getFeedUrlFrom(@Nonnull final String channelIdOrUser) {
|
||||||
if (channelIdOrUser.startsWith("user/")) {
|
if (channelIdOrUser.startsWith("user/")) {
|
||||||
return FEED_BASE_USER + channelIdOrUser.replace("user/", "");
|
return FEED_BASE_USER + channelIdOrUser.replace("user/", "");
|
||||||
} else if (channelIdOrUser.startsWith("channel/")) {
|
} else if (channelIdOrUser.startsWith("channel/")) {
|
||||||
@ -204,14 +219,16 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OffsetDateTime parseDateFrom(final String textualUploadDate) throws ParsingException {
|
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
|
||||||
|
throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return OffsetDateTime.parse(textualUploadDate);
|
return OffsetDateTime.parse(textualUploadDate);
|
||||||
} catch (DateTimeParseException e) {
|
} catch (final DateTimeParseException e) {
|
||||||
try {
|
try {
|
||||||
return LocalDate.parse(textualUploadDate).atStartOfDay().atOffset(ZoneOffset.UTC);
|
return LocalDate.parse(textualUploadDate).atStartOfDay().atOffset(ZoneOffset.UTC);
|
||||||
} catch (DateTimeParseException e1) {
|
} catch (final DateTimeParseException e1) {
|
||||||
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"", e1);
|
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"",
|
||||||
|
e1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,10 +237,10 @@ public class YoutubeParsingHelper {
|
|||||||
* Checks if the given playlist id is a YouTube Mix (auto-generated playlist)
|
* Checks if the given playlist id is a YouTube Mix (auto-generated playlist)
|
||||||
* Ids from a YouTube Mix start with "RD"
|
* Ids from a YouTube Mix start with "RD"
|
||||||
*
|
*
|
||||||
* @param playlistId
|
* @param playlistId the playlist id
|
||||||
* @return Whether given id belongs to a YouTube Mix
|
* @return Whether given id belongs to a YouTube Mix
|
||||||
*/
|
*/
|
||||||
public static boolean isYoutubeMixId(final String playlistId) {
|
public static boolean isYoutubeMixId(@Nonnull final String playlistId) {
|
||||||
return playlistId.startsWith("RD") && !isYoutubeMusicMixId(playlistId);
|
return playlistId.startsWith("RD") && !isYoutubeMusicMixId(playlistId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,10 +248,10 @@ public class YoutubeParsingHelper {
|
|||||||
* Checks if the given playlist id is a YouTube Music Mix (auto-generated playlist)
|
* Checks if the given playlist id is a YouTube Music Mix (auto-generated playlist)
|
||||||
* Ids from a YouTube Music Mix start with "RDAMVM" or "RDCLAK"
|
* Ids from a YouTube Music Mix start with "RDAMVM" or "RDCLAK"
|
||||||
*
|
*
|
||||||
* @param playlistId
|
* @param playlistId the playlist id
|
||||||
* @return Whether given id belongs to a YouTube Music Mix
|
* @return Whether given id belongs to a YouTube Music Mix
|
||||||
*/
|
*/
|
||||||
public static boolean isYoutubeMusicMixId(final String playlistId) {
|
public static boolean isYoutubeMusicMixId(@Nonnull final String playlistId) {
|
||||||
return playlistId.startsWith("RDAMVM") || playlistId.startsWith("RDCLAK");
|
return playlistId.startsWith("RDAMVM") || playlistId.startsWith("RDCLAK");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +261,7 @@ public class YoutubeParsingHelper {
|
|||||||
*
|
*
|
||||||
* @return Whether given id belongs to a YouTube Channel Mix
|
* @return Whether given id belongs to a YouTube Channel Mix
|
||||||
*/
|
*/
|
||||||
public static boolean isYoutubeChannelMixId(final String playlistId) {
|
public static boolean isYoutubeChannelMixId(@Nonnull final String playlistId) {
|
||||||
return playlistId.startsWith("RDCM");
|
return playlistId.startsWith("RDCM");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +270,9 @@ public class YoutubeParsingHelper {
|
|||||||
*
|
*
|
||||||
* @throws ParsingException If the playlistId is a Channel Mix or not a mix.
|
* @throws ParsingException If the playlistId is a Channel Mix or not a mix.
|
||||||
*/
|
*/
|
||||||
public static String extractVideoIdFromMixId(final String playlistId) throws ParsingException {
|
@Nonnull
|
||||||
|
public static String extractVideoIdFromMixId(@Nonnull final String playlistId)
|
||||||
|
throws ParsingException {
|
||||||
if (playlistId.startsWith("RDMM")) { // My Mix
|
if (playlistId.startsWith("RDMM")) { // My Mix
|
||||||
return playlistId.substring(4);
|
return playlistId.substring(4);
|
||||||
|
|
||||||
@ -262,49 +281,88 @@ public class YoutubeParsingHelper {
|
|||||||
|
|
||||||
} else if (isYoutubeChannelMixId(playlistId)) { // starts with "RMCM"
|
} else if (isYoutubeChannelMixId(playlistId)) { // starts with "RMCM"
|
||||||
// Channel mix are build with RMCM{channelId}, so videoId can't be determined
|
// Channel mix are build with RMCM{channelId}, so videoId can't be determined
|
||||||
throw new ParsingException("Video id could not be determined from mix id: " + playlistId);
|
throw new ParsingException("Video id could not be determined from mix id: "
|
||||||
|
+ playlistId);
|
||||||
|
|
||||||
} else if (isYoutubeMixId(playlistId)) { // normal mix, starts with "RD"
|
} else if (isYoutubeMixId(playlistId)) { // normal mix, starts with "RD"
|
||||||
return playlistId.substring(2);
|
return playlistId.substring(2);
|
||||||
|
|
||||||
} else { // not a mix
|
} else { // not a mix
|
||||||
throw new ParsingException("Video id could not be determined from mix id: " + playlistId);
|
throw new ParsingException("Video id could not be determined from mix id: "
|
||||||
|
+ playlistId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JsonObject getInitialData(final String html) throws ParsingException {
|
public static JsonObject getInitialData(final String html) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
final String initialData = Parser.matchGroup1("window\\[\"ytInitialData\"\\]\\s*=\\s*(\\{.*?\\});", html);
|
final String initialData = Parser.matchGroup1(
|
||||||
|
"window\\[\"ytInitialData\"\\]\\s*=\\s*(\\{.*?\\});", html);
|
||||||
return JsonParser.object().from(initialData);
|
return JsonParser.object().from(initialData);
|
||||||
} catch (Parser.RegexException e) {
|
} catch (final Parser.RegexException e) {
|
||||||
final String initialData = Parser.matchGroup1("var\\s*ytInitialData\\s*=\\s*(\\{.*?\\});", html);
|
final String initialData = Parser.matchGroup1(
|
||||||
|
"var\\s*ytInitialData\\s*=\\s*(\\{.*?\\});", html);
|
||||||
return JsonParser.object().from(initialData);
|
return JsonParser.object().from(initialData);
|
||||||
}
|
}
|
||||||
} catch (JsonParserException | Parser.RegexException e) {
|
} catch (final JsonParserException | Parser.RegexException e) {
|
||||||
throw new ParsingException("Could not get ytInitialData", e);
|
throw new ParsingException("Could not get ytInitialData", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isHardcodedClientVersionValid() throws IOException, ExtractionException {
|
public static boolean areHardcodedClientVersionAndKeyValid()
|
||||||
final String url = "https://www.youtube.com/results?search_query=test&pbj=1";
|
throws IOException, ExtractionException {
|
||||||
|
if (hardcodedClientVersionAndKeyValid.isPresent()) {
|
||||||
|
return hardcodedClientVersionAndKeyValid.get();
|
||||||
|
}
|
||||||
|
// @formatter:off
|
||||||
|
final byte[] body = JsonWriter.string()
|
||||||
|
.object()
|
||||||
|
.object("context")
|
||||||
|
.object("client")
|
||||||
|
.value("hl", "en-GB")
|
||||||
|
.value("gl", "GB")
|
||||||
|
.value("clientName", "WEB")
|
||||||
|
.value("clientVersion", HARDCODED_CLIENT_VERSION)
|
||||||
|
.end()
|
||||||
|
.object("user")
|
||||||
|
.value("lockedSafetyMode", false)
|
||||||
|
.end()
|
||||||
|
.value("fetchLiveState", true)
|
||||||
|
.end()
|
||||||
|
.end().done().getBytes(UTF_8);
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
final Map<String, List<String>> headers = new HashMap<>();
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
headers.put("X-YouTube-Client-Name", Collections.singletonList("1"));
|
headers.put("X-YouTube-Client-Name", Collections.singletonList("1"));
|
||||||
headers.put("X-YouTube-Client-Version", Collections.singletonList(HARDCODED_CLIENT_VERSION));
|
headers.put("X-YouTube-Client-Version",
|
||||||
final String response = getDownloader().get(url, headers).responseBody();
|
Collections.singletonList(HARDCODED_CLIENT_VERSION));
|
||||||
|
|
||||||
return response.length() > 50; // ensure to have a valid response
|
// 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, headers, body);
|
||||||
|
final String responseBody = response.responseBody();
|
||||||
|
final int responseCode = response.responseCode();
|
||||||
|
|
||||||
|
hardcodedClientVersionAndKeyValid = Optional.of(responseBody.length() > 5000
|
||||||
|
&& responseCode == 200); // Ensure to have a valid response
|
||||||
|
return hardcodedClientVersionAndKeyValid.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void extractClientVersionAndKey() throws IOException, ExtractionException {
|
private static void extractClientVersionAndKey() throws IOException, ExtractionException {
|
||||||
final String url = "https://www.youtube.com/results?search_query=test";
|
// Don't extract the client version and the InnerTube key if it has been already extracted
|
||||||
final String html = getDownloader().get(url).responseBody();
|
if (keyAndVersionExtracted) return;
|
||||||
|
// Don't provide a search term in order to have a smaller response
|
||||||
|
final String url = "https://www.youtube.com/results?search_query=&ucbcb=1";
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
addCookieHeader(headers);
|
||||||
|
final String html = getDownloader().get(url, headers).responseBody();
|
||||||
final JsonObject initialData = getInitialData(html);
|
final JsonObject initialData = getInitialData(html);
|
||||||
final JsonArray serviceTrackingParams = initialData.getObject("responseContext").getArray("serviceTrackingParams");
|
final JsonArray serviceTrackingParams = initialData.getObject("responseContext")
|
||||||
|
.getArray("serviceTrackingParams");
|
||||||
String shortClientVersion = null;
|
String shortClientVersion = null;
|
||||||
|
|
||||||
// try to get version from initial data first
|
// Try to get version from initial data first
|
||||||
for (final Object service : serviceTrackingParams) {
|
for (final Object service : serviceTrackingParams) {
|
||||||
final JsonObject s = (JsonObject) service;
|
final JsonObject s = (JsonObject) service;
|
||||||
if (s.getString("service").equals("CSI")) {
|
if (s.getString("service").equals("CSI")) {
|
||||||
@ -317,7 +375,8 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (s.getString("service").equals("ECATCHER")) {
|
} else if (s.getString("service").equals("ECATCHER")) {
|
||||||
// fallback to get a shortened client version which does not contain the last two digits
|
// Fallback to get a shortened client version which does not contain the last two
|
||||||
|
// digits
|
||||||
final JsonArray params = s.getArray("params");
|
final JsonArray params = s.getArray("params");
|
||||||
for (final Object param : params) {
|
for (final Object param : params) {
|
||||||
final JsonObject p = (JsonObject) param;
|
final JsonObject p = (JsonObject) param;
|
||||||
@ -342,7 +401,7 @@ public class YoutubeParsingHelper {
|
|||||||
clientVersion = contextClientVersion;
|
clientVersion = contextClientVersion;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (Parser.RegexException ignored) {
|
} catch (final Parser.RegexException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,12 +411,14 @@ public class YoutubeParsingHelper {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
key = Parser.matchGroup1("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", html);
|
key = Parser.matchGroup1("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", html);
|
||||||
} catch (Parser.RegexException e) {
|
} catch (final Parser.RegexException e1) {
|
||||||
try {
|
try {
|
||||||
key = Parser.matchGroup1("innertubeApiKey\":\"([0-9a-zA-Z_-]+?)\"", html);
|
key = Parser.matchGroup1("innertubeApiKey\":\"([0-9a-zA-Z_-]+?)\"", html);
|
||||||
} catch (Parser.RegexException ignored) {
|
} catch (final Parser.RegexException e2) {
|
||||||
|
throw new ParsingException("Could not extract client version and key");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
keyAndVersionExtracted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -365,10 +426,11 @@ public class YoutubeParsingHelper {
|
|||||||
*/
|
*/
|
||||||
public static String getClientVersion() throws IOException, ExtractionException {
|
public static String getClientVersion() throws IOException, ExtractionException {
|
||||||
if (!isNullOrEmpty(clientVersion)) return clientVersion;
|
if (!isNullOrEmpty(clientVersion)) return clientVersion;
|
||||||
if (isHardcodedClientVersionValid()) return clientVersion = HARDCODED_CLIENT_VERSION;
|
if (areHardcodedClientVersionAndKeyValid()) {
|
||||||
|
return clientVersion = HARDCODED_CLIENT_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
extractClientVersionAndKey();
|
extractClientVersionAndKey();
|
||||||
if (isNullOrEmpty(key)) throw new ParsingException("Could not extract client version");
|
|
||||||
return clientVersion;
|
return clientVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,9 +439,11 @@ public class YoutubeParsingHelper {
|
|||||||
*/
|
*/
|
||||||
public static String getKey() throws IOException, ExtractionException {
|
public static String getKey() throws IOException, ExtractionException {
|
||||||
if (!isNullOrEmpty(key)) return key;
|
if (!isNullOrEmpty(key)) return key;
|
||||||
|
if (areHardcodedClientVersionAndKeyValid()) {
|
||||||
|
return key = HARDCODED_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
extractClientVersionAndKey();
|
extractClientVersionAndKey();
|
||||||
if (isNullOrEmpty(key)) throw new ParsingException("Could not extract key");
|
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,12 +472,15 @@ public class YoutubeParsingHelper {
|
|||||||
* <b>Only use in tests.</b>
|
* <b>Only use in tests.</b>
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public static void setNumberGenerator(Random random) {
|
public static void setNumberGenerator(final Random random) {
|
||||||
numberGenerator = random;
|
numberGenerator = random;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean areHardcodedYoutubeMusicKeysValid() throws IOException, ReCaptchaException {
|
public static boolean isHardcodedYoutubeMusicKeyValid() throws IOException,
|
||||||
final String url = "https://music.youtube.com/youtubei/v1/search?alt=json&key=" + HARDCODED_YOUTUBE_MUSIC_KEYS[0];
|
ReCaptchaException {
|
||||||
|
final String url =
|
||||||
|
"https://music.youtube.com/youtubei/v1/music/get_search_suggestions?alt=json&key="
|
||||||
|
+ HARDCODED_YOUTUBE_MUSIC_KEY[0];
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
byte[] json = JsonWriter.string()
|
byte[] json = JsonWriter.string()
|
||||||
@ -421,12 +488,11 @@ public class YoutubeParsingHelper {
|
|||||||
.object("context")
|
.object("context")
|
||||||
.object("client")
|
.object("client")
|
||||||
.value("clientName", "WEB_REMIX")
|
.value("clientName", "WEB_REMIX")
|
||||||
.value("clientVersion", HARDCODED_YOUTUBE_MUSIC_KEYS[2])
|
.value("clientVersion", HARDCODED_YOUTUBE_MUSIC_KEY[2])
|
||||||
.value("hl", "en")
|
.value("hl", "en-GB")
|
||||||
.value("gl", "GB")
|
.value("gl", "GB")
|
||||||
.array("experimentIds").end()
|
.array("experimentIds").end()
|
||||||
.value("experimentsToken", "")
|
.value("experimentsToken", EMPTY_STRING)
|
||||||
.value("utcOffsetMinutes", 0)
|
|
||||||
.object("locationInfo").end()
|
.object("locationInfo").end()
|
||||||
.object("musicAppInfo").end()
|
.object("musicAppInfo").end()
|
||||||
.end()
|
.end()
|
||||||
@ -440,58 +506,66 @@ public class YoutubeParsingHelper {
|
|||||||
.value("enableSafetyMode", false)
|
.value("enableSafetyMode", false)
|
||||||
.end()
|
.end()
|
||||||
.end()
|
.end()
|
||||||
.value("query", "test")
|
.value("input", "")
|
||||||
.value("params", "Eg-KAQwIARAAGAAgACgAMABqChAEEAUQAxAKEAk%3D")
|
|
||||||
.end().done().getBytes(UTF_8);
|
.end().done().getBytes(UTF_8);
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
final Map<String, List<String>> headers = new HashMap<>();
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
headers.put("X-YouTube-Client-Name", Collections.singletonList(HARDCODED_YOUTUBE_MUSIC_KEYS[1]));
|
headers.put("X-YouTube-Client-Name", Collections.singletonList(
|
||||||
headers.put("X-YouTube-Client-Version", Collections.singletonList(HARDCODED_YOUTUBE_MUSIC_KEYS[2]));
|
HARDCODED_YOUTUBE_MUSIC_KEY[1]));
|
||||||
|
headers.put("X-YouTube-Client-Version", Collections.singletonList(
|
||||||
|
HARDCODED_YOUTUBE_MUSIC_KEY[2]));
|
||||||
headers.put("Origin", Collections.singletonList("https://music.youtube.com"));
|
headers.put("Origin", Collections.singletonList("https://music.youtube.com"));
|
||||||
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
||||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||||
addCookieHeader(headers);
|
|
||||||
|
|
||||||
final String response = getDownloader().post(url, headers, json).responseBody();
|
final Response response = getDownloader().post(url, headers, json);
|
||||||
|
// Ensure to have a valid response
|
||||||
return response.length() > 50; // ensure to have a valid response
|
return response.responseBody().length() > 500 && response.responseCode() == 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getYoutubeMusicKeys() throws IOException, ReCaptchaException, Parser.RegexException {
|
public static String[] getYoutubeMusicKey() throws IOException, ReCaptchaException,
|
||||||
if (youtubeMusicKeys != null && youtubeMusicKeys.length == 3) return youtubeMusicKeys;
|
Parser.RegexException {
|
||||||
if (areHardcodedYoutubeMusicKeysValid()) return youtubeMusicKeys = HARDCODED_YOUTUBE_MUSIC_KEYS;
|
if (youtubeMusicKey != null && youtubeMusicKey.length == 3) return youtubeMusicKey;
|
||||||
|
if (isHardcodedYoutubeMusicKeyValid()) {
|
||||||
|
return youtubeMusicKey = HARDCODED_YOUTUBE_MUSIC_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
final String url = "https://music.youtube.com/";
|
final String url = "https://music.youtube.com/";
|
||||||
final String html = getDownloader().get(url).responseBody();
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
addCookieHeader(headers);
|
||||||
|
final String html = getDownloader().get(url, headers).responseBody();
|
||||||
|
|
||||||
String key;
|
String key;
|
||||||
try {
|
try {
|
||||||
key = Parser.matchGroup1("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", html);
|
key = Parser.matchGroup1("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"", html);
|
||||||
} catch (Parser.RegexException e) {
|
} catch (final Parser.RegexException e) {
|
||||||
key = Parser.matchGroup1("innertube_api_key\":\"([0-9a-zA-Z_-]+?)\"", html);
|
key = Parser.matchGroup1("innertube_api_key\":\"([0-9a-zA-Z_-]+?)\"", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String clientName = Parser.matchGroup1("INNERTUBE_CONTEXT_CLIENT_NAME\":([0-9]+?),", html);
|
final String clientName = Parser.matchGroup1("INNERTUBE_CONTEXT_CLIENT_NAME\":([0-9]+?),",
|
||||||
|
html);
|
||||||
|
|
||||||
String clientVersion;
|
String clientVersion;
|
||||||
try {
|
try {
|
||||||
clientVersion = Parser.matchGroup1("INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"", html);
|
clientVersion = Parser.matchGroup1(
|
||||||
} catch (Parser.RegexException e) {
|
"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"", html);
|
||||||
|
} catch (final Parser.RegexException e) {
|
||||||
try {
|
try {
|
||||||
clientVersion = Parser.matchGroup1("INNERTUBE_CLIENT_VERSION\":\"([0-9\\.]+?)\"", html);
|
clientVersion = Parser.matchGroup1(
|
||||||
} catch (Parser.RegexException ee) {
|
"INNERTUBE_CLIENT_VERSION\":\"([0-9\\.]+?)\"", html);
|
||||||
clientVersion = Parser.matchGroup1("innertube_context_client_version\":\"([0-9\\.]+?)\"", html);
|
} catch (final Parser.RegexException ee) {
|
||||||
|
clientVersion = Parser.matchGroup1(
|
||||||
|
"innertube_context_client_version\":\"([0-9\\.]+?)\"", html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return youtubeMusicKeys = new String[]{key, clientName, clientVersion};
|
return youtubeMusicKey = new String[]{key, clientName, clientVersion};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getUrlFromNavigationEndpoint(JsonObject navigationEndpoint) throws ParsingException {
|
public static String getUrlFromNavigationEndpoint(@Nonnull final JsonObject navigationEndpoint)
|
||||||
|
throws ParsingException {
|
||||||
if (navigationEndpoint.has("urlEndpoint")) {
|
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?")) {
|
if (internUrl.startsWith("https://www.youtube.com/redirect?")) {
|
||||||
@ -508,7 +582,7 @@ public class YoutubeParsingHelper {
|
|||||||
String url;
|
String url;
|
||||||
try {
|
try {
|
||||||
url = URLDecoder.decode(param.split("=")[1], UTF_8);
|
url = URLDecoder.decode(param.split("=")[1], UTF_8);
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (final UnsupportedEncodingException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
@ -516,7 +590,8 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
} else if (internUrl.startsWith("http")) {
|
} else if (internUrl.startsWith("http")) {
|
||||||
return internUrl;
|
return internUrl;
|
||||||
} else if (internUrl.startsWith("/channel") || internUrl.startsWith("/user") || internUrl.startsWith("/watch")) {
|
} else if (internUrl.startsWith("/channel") || internUrl.startsWith("/user")
|
||||||
|
|| internUrl.startsWith("/watch")) {
|
||||||
return "https://www.youtube.com" + internUrl;
|
return "https://www.youtube.com" + internUrl;
|
||||||
}
|
}
|
||||||
} else if (navigationEndpoint.has("browseEndpoint")) {
|
} else if (navigationEndpoint.has("browseEndpoint")) {
|
||||||
@ -533,10 +608,12 @@ public class YoutubeParsingHelper {
|
|||||||
return "https://www.youtube.com" + canonicalBaseUrl;
|
return "https://www.youtube.com" + canonicalBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParsingException("canonicalBaseUrl is null and browseId is not a channel (\"" + browseEndpoint + "\")");
|
throw new ParsingException("canonicalBaseUrl is null and browseId is not a channel (\""
|
||||||
|
+ browseEndpoint + "\")");
|
||||||
} else if (navigationEndpoint.has("watchEndpoint")) {
|
} else if (navigationEndpoint.has("watchEndpoint")) {
|
||||||
StringBuilder url = new StringBuilder();
|
StringBuilder url = new StringBuilder();
|
||||||
url.append("https://www.youtube.com/watch?v=").append(navigationEndpoint.getObject("watchEndpoint").getString("videoId"));
|
url.append("https://www.youtube.com/watch?v=").append(navigationEndpoint
|
||||||
|
.getObject("watchEndpoint").getString("videoId"));
|
||||||
if (navigationEndpoint.getObject("watchEndpoint").has("playlistId")) {
|
if (navigationEndpoint.getObject("watchEndpoint").has("playlistId")) {
|
||||||
url.append("&list=").append(navigationEndpoint.getObject("watchEndpoint")
|
url.append("&list=").append(navigationEndpoint.getObject("watchEndpoint")
|
||||||
.getString("playlistId"));
|
.getString("playlistId"));
|
||||||
@ -561,7 +638,8 @@ public class YoutubeParsingHelper {
|
|||||||
* @return text in the JSON object or {@code null}
|
* @return text in the JSON object or {@code null}
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getTextFromObject(JsonObject textObject, boolean html) throws ParsingException {
|
public static String getTextFromObject(final JsonObject textObject, final boolean html)
|
||||||
|
throws ParsingException {
|
||||||
if (isNullOrEmpty(textObject)) return null;
|
if (isNullOrEmpty(textObject)) return null;
|
||||||
|
|
||||||
if (textObject.has("simpleText")) return textObject.getString("simpleText");
|
if (textObject.has("simpleText")) return textObject.getString("simpleText");
|
||||||
@ -572,9 +650,11 @@ public class YoutubeParsingHelper {
|
|||||||
for (final Object textPart : textObject.getArray("runs")) {
|
for (final Object textPart : textObject.getArray("runs")) {
|
||||||
String text = ((JsonObject) textPart).getString("text");
|
String text = ((JsonObject) textPart).getString("text");
|
||||||
if (html && ((JsonObject) textPart).has("navigationEndpoint")) {
|
if (html && ((JsonObject) textPart).has("navigationEndpoint")) {
|
||||||
String url = getUrlFromNavigationEndpoint(((JsonObject) textPart).getObject("navigationEndpoint"));
|
String url = getUrlFromNavigationEndpoint(((JsonObject) textPart)
|
||||||
|
.getObject("navigationEndpoint"));
|
||||||
if (!isNullOrEmpty(url)) {
|
if (!isNullOrEmpty(url)) {
|
||||||
textBuilder.append("<a href=\"").append(url).append("\">").append(text).append("</a>");
|
textBuilder.append("<a href=\"").append(url).append("\">").append(text)
|
||||||
|
.append("</a>");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -592,12 +672,12 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getTextFromObject(JsonObject textObject) throws ParsingException {
|
public static String getTextFromObject(final JsonObject textObject) throws ParsingException {
|
||||||
return getTextFromObject(textObject, false);
|
return getTextFromObject(textObject, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getTextAtKey(final JsonObject jsonObject, final String key)
|
public static String getTextAtKey(@Nonnull final JsonObject jsonObject, final String key)
|
||||||
throws ParsingException {
|
throws ParsingException {
|
||||||
if (jsonObject.isString(key)) {
|
if (jsonObject.isString(key)) {
|
||||||
return jsonObject.getString(key);
|
return jsonObject.getString(key);
|
||||||
@ -606,7 +686,7 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String fixThumbnailUrl(String thumbnailUrl) {
|
public static String fixThumbnailUrl(@Nonnull String thumbnailUrl) {
|
||||||
if (thumbnailUrl.startsWith("//")) {
|
if (thumbnailUrl.startsWith("//")) {
|
||||||
thumbnailUrl = thumbnailUrl.substring(2);
|
thumbnailUrl = thumbnailUrl.substring(2);
|
||||||
}
|
}
|
||||||
@ -620,7 +700,8 @@ public class YoutubeParsingHelper {
|
|||||||
return thumbnailUrl;
|
return thumbnailUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getValidJsonResponseBody(final Response response)
|
@Nonnull
|
||||||
|
public static String getValidJsonResponseBody(@Nonnull final Response response)
|
||||||
throws ParsingException, MalformedURLException {
|
throws ParsingException, MalformedURLException {
|
||||||
if (response.responseCode() == 404) {
|
if (response.responseCode() == 404) {
|
||||||
throw new ContentNotAvailableException("Not found"
|
throw new ContentNotAvailableException("Not found"
|
||||||
@ -628,7 +709,7 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String responseBody = response.responseBody();
|
final String responseBody = response.responseBody();
|
||||||
if (responseBody.length() < 50) { // ensure to have a valid response
|
if (responseBody.length() < 50) { // Ensure to have a valid response
|
||||||
throw new ParsingException("JSON response is too short");
|
throw new ParsingException("JSON response is too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,6 +743,41 @@ public class YoutubeParsingHelper {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JsonObject getJsonPostResponse(final String endpoint,
|
||||||
|
final byte[] body,
|
||||||
|
final Localization localization)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
addClientInfoHeaders(headers);
|
||||||
|
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||||
|
|
||||||
|
final Response response = getDownloader().post(YOUTUBEI_V1_URL + endpoint + "?key="
|
||||||
|
+ getKey(), headers, body, localization);
|
||||||
|
|
||||||
|
return JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonObject getJsonMobilePostResponse(final String endpoint,
|
||||||
|
final byte[] body,
|
||||||
|
@Nonnull final ContentCountry
|
||||||
|
contentCountry,
|
||||||
|
final Localization localization)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||||
|
// Spoofing an Android 11 device with the hardcoded version of the Android app
|
||||||
|
headers.put("User-Agent", Collections.singletonList("com.google.android.youtube/"
|
||||||
|
+ MOBILE_YOUTUBE_CLIENT_VERSION + "Linux; U; Android 11; "
|
||||||
|
+ contentCountry.getCountryCode() + ") gzip"));
|
||||||
|
headers.put("x-goog-api-format-version", Collections.singletonList("2"));
|
||||||
|
|
||||||
|
final Response response = getDownloader().post(
|
||||||
|
"https://youtubei.googleapis.com/youtubei/v1/" + endpoint + "?key="
|
||||||
|
+ MOBILE_YOUTUBE_KEY, headers, body, localization);
|
||||||
|
|
||||||
|
return JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||||
|
}
|
||||||
|
|
||||||
public static JsonArray getJsonResponse(final String url, final Localization localization)
|
public static JsonArray getJsonResponse(final String url, final Localization localization)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
Map<String, List<String>> headers = new HashMap<>();
|
Map<String, List<String>> headers = new HashMap<>();
|
||||||
@ -672,7 +788,8 @@ public class YoutubeParsingHelper {
|
|||||||
return JsonUtils.toJsonArray(getValidJsonResponseBody(response));
|
return JsonUtils.toJsonArray(getValidJsonResponseBody(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JsonArray getJsonResponse(final Page page, final Localization localization)
|
public static JsonArray getJsonResponse(@Nonnull final Page page,
|
||||||
|
final Localization localization)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
final Map<String, List<String>> headers = new HashMap<>();
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
addYouTubeHeaders(headers);
|
addYouTubeHeaders(headers);
|
||||||
@ -682,19 +799,140 @@ public class YoutubeParsingHelper {
|
|||||||
return JsonUtils.toJsonArray(getValidJsonResponseBody(response));
|
return JsonUtils.toJsonArray(getValidJsonResponseBody(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JsonBuilder<JsonObject> prepareJsonBuilder()
|
@Nonnull
|
||||||
|
public static JsonBuilder<JsonObject> prepareDesktopJsonBuilder(
|
||||||
|
@Nonnull final Localization localization,
|
||||||
|
@Nonnull final ContentCountry contentCountry)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
return JsonObject.builder()
|
return JsonObject.builder()
|
||||||
.object("context")
|
.object("context")
|
||||||
.object("client")
|
.object("client")
|
||||||
.value("clientName", "1")
|
.value("hl", localization.getLocalizationCode())
|
||||||
|
.value("gl", contentCountry.getCountryCode())
|
||||||
|
.value("clientName", "WEB")
|
||||||
.value("clientVersion", getClientVersion())
|
.value("clientVersion", getClientVersion())
|
||||||
.end()
|
.end()
|
||||||
|
.object("user")
|
||||||
|
// TO DO: provide a way to enable restricted mode with:
|
||||||
|
// .value("enableSafetyMode", boolean)
|
||||||
|
.value("lockedSafetyMode", false)
|
||||||
|
.end()
|
||||||
.end();
|
.end();
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static JsonBuilder<JsonObject> prepareAndroidMobileJsonBuilder(
|
||||||
|
@Nonnull final Localization localization,
|
||||||
|
@Nonnull final ContentCountry contentCountry) {
|
||||||
|
// @formatter:off
|
||||||
|
return JsonObject.builder()
|
||||||
|
.object("context")
|
||||||
|
.object("client")
|
||||||
|
.value("clientName", "ANDROID")
|
||||||
|
.value("clientVersion", MOBILE_YOUTUBE_CLIENT_VERSION)
|
||||||
|
.value("hl", localization.getLocalizationCode())
|
||||||
|
.value("gl", contentCountry.getCountryCode())
|
||||||
|
.end()
|
||||||
|
.object("user")
|
||||||
|
// TO DO: provide a way to enable restricted mode with:
|
||||||
|
// .value("enableSafetyMode", boolean)
|
||||||
|
.value("lockedSafetyMode", false)
|
||||||
|
.end()
|
||||||
|
.end();
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static JsonBuilder<JsonObject> prepareDesktopEmbedVideoJsonBuilder(
|
||||||
|
@Nonnull final Localization localization,
|
||||||
|
@Nonnull final ContentCountry contentCountry,
|
||||||
|
@Nonnull final String videoId) throws IOException, ExtractionException {
|
||||||
|
// @formatter:off
|
||||||
|
return JsonObject.builder()
|
||||||
|
.object("context")
|
||||||
|
.object("client")
|
||||||
|
.value("hl", localization.getLocalizationCode())
|
||||||
|
.value("gl", contentCountry.getCountryCode())
|
||||||
|
.value("clientName", "WEB")
|
||||||
|
.value("clientVersion", getClientVersion())
|
||||||
|
.value("clientScreen", "EMBED")
|
||||||
|
.end()
|
||||||
|
.object("thirdParty")
|
||||||
|
.value("embedUrl", "https://www.youtube.com/watch?v=" + videoId)
|
||||||
|
.end()
|
||||||
|
.object("user")
|
||||||
|
// TO DO: provide a way to enable restricted mode with:
|
||||||
|
// .value("enableSafetyMode", boolean)
|
||||||
|
.value("lockedSafetyMode", false)
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.value("videoId", videoId);
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static JsonBuilder<JsonObject> prepareAndroidMobileEmbedVideoJsonBuilder(
|
||||||
|
@Nonnull final Localization localization,
|
||||||
|
@Nonnull final ContentCountry contentCountry,
|
||||||
|
@Nonnull final String videoId) {
|
||||||
|
// @formatter:off
|
||||||
|
return JsonObject.builder()
|
||||||
|
.object("context")
|
||||||
|
.object("client")
|
||||||
|
.value("clientName", "ANDROID")
|
||||||
|
.value("clientVersion", MOBILE_YOUTUBE_CLIENT_VERSION)
|
||||||
|
.value("clientScreen", "EMBED")
|
||||||
|
.value("hl", localization.getLocalizationCode())
|
||||||
|
.value("gl", contentCountry.getCountryCode())
|
||||||
|
.end()
|
||||||
|
.object("thirdParty")
|
||||||
|
.value("embedUrl", "https://www.youtube.com/watch?v=" + videoId)
|
||||||
|
.end()
|
||||||
|
.object("user")
|
||||||
|
// TO DO: provide a way to enable restricted mode with:
|
||||||
|
// .value("enableSafetyMode", boolean)
|
||||||
|
.value("lockedSafetyMode", false)
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.value("videoId", videoId);
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static byte[] createPlayerBodyWithSts(final Localization localization,
|
||||||
|
final ContentCountry contentCountry,
|
||||||
|
final String videoId,
|
||||||
|
final boolean withThirdParty,
|
||||||
|
@Nullable final String sts)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
if (withThirdParty) {
|
||||||
|
// @formatter:off
|
||||||
|
return JsonWriter.string(prepareDesktopEmbedVideoJsonBuilder(localization, contentCountry, videoId)
|
||||||
|
.object("playbackContext")
|
||||||
|
.object("contentPlaybackContext")
|
||||||
|
.value("signatureTimestamp", sts)
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
// @formatter:on
|
||||||
|
} else {
|
||||||
|
// @formatter:off
|
||||||
|
return JsonWriter.string(prepareDesktopJsonBuilder(localization, contentCountry)
|
||||||
|
.value("videoId", videoId)
|
||||||
|
.object("playbackContext")
|
||||||
|
.object("contentPlaybackContext")
|
||||||
|
.value("signatureTimestamp", sts)
|
||||||
|
.end()
|
||||||
|
.end()
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add required headers and cookies to an existing headers Map.
|
* Add required headers and cookies to an existing headers Map.
|
||||||
* @see #addClientInfoHeaders(Map)
|
* @see #addClientInfoHeaders(Map)
|
||||||
@ -707,14 +945,17 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the <code>X-YouTube-Client-Name</code> and <code>X-YouTube-Client-Version</code> 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
|
* @param headers The headers which should be completed
|
||||||
*/
|
*/
|
||||||
public static void addClientInfoHeaders(final Map<String, List<String>> headers)
|
public static void addClientInfoHeaders(@Nonnull final Map<String, List<String>> headers)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
if (headers.get("X-YouTube-Client-Name") == null) {
|
headers.computeIfAbsent("Origin", k -> Collections.singletonList(
|
||||||
headers.put("X-YouTube-Client-Name", Collections.singletonList("1"));
|
"https://www.youtube.com"));
|
||||||
}
|
headers.computeIfAbsent("Referer", k -> Collections.singletonList(
|
||||||
|
"https://www.youtube.com"));
|
||||||
|
headers.computeIfAbsent("X-YouTube-Client-Name", k -> Collections.singletonList("1"));
|
||||||
if (headers.get("X-YouTube-Client-Version") == null) {
|
if (headers.get("X-YouTube-Client-Version") == null) {
|
||||||
headers.put("X-YouTube-Client-Version", Collections.singletonList(getClientVersion()));
|
headers.put("X-YouTube-Client-Version", Collections.singletonList(getClientVersion()));
|
||||||
}
|
}
|
||||||
@ -725,7 +966,7 @@ public class YoutubeParsingHelper {
|
|||||||
* @see #CONSENT_COOKIE
|
* @see #CONSENT_COOKIE
|
||||||
* @param headers the headers which should be completed
|
* @param headers the headers which should be completed
|
||||||
*/
|
*/
|
||||||
public static void addCookieHeader(final Map<String, List<String>> headers) {
|
public static void addCookieHeader(@Nonnull final Map<String, List<String>> headers) {
|
||||||
if (headers.get("Cookie") == null) {
|
if (headers.get("Cookie") == null) {
|
||||||
headers.put("Cookie", Arrays.asList(generateConsentCookie()));
|
headers.put("Cookie", Arrays.asList(generateConsentCookie()));
|
||||||
} else {
|
} else {
|
||||||
@ -733,12 +974,14 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
public static String generateConsentCookie() {
|
public static String generateConsentCookie() {
|
||||||
final int statusCode = 100 + numberGenerator.nextInt(900);
|
final int statusCode = 100 + numberGenerator.nextInt(900);
|
||||||
return CONSENT_COOKIE + statusCode;
|
return CONSENT_COOKIE + statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String extractCookieValue(final String cookieName, final Response response) {
|
public static String extractCookieValue(final String cookieName,
|
||||||
|
@Nonnull final Response response) {
|
||||||
final List<String> cookies = response.responseHeaders().get("set-cookie");
|
final List<String> cookies = response.responseHeaders().get("set-cookie");
|
||||||
int startIndex;
|
int startIndex;
|
||||||
String result = "";
|
String result = "";
|
||||||
@ -761,7 +1004,8 @@ public class YoutubeParsingHelper {
|
|||||||
* @param initialData the object which will be checked if an alert is present
|
* @param initialData the object which will be checked if an alert is present
|
||||||
* @throws ContentNotAvailableException if an alert is detected
|
* @throws ContentNotAvailableException if an alert is detected
|
||||||
*/
|
*/
|
||||||
public static void defaultAlertsCheck(final JsonObject initialData) throws ParsingException {
|
public static void defaultAlertsCheck(@Nonnull final JsonObject initialData)
|
||||||
|
throws ParsingException {
|
||||||
final JsonArray alerts = initialData.getArray("alerts");
|
final JsonArray alerts = initialData.getArray("alerts");
|
||||||
if (!isNullOrEmpty(alerts)) {
|
if (!isNullOrEmpty(alerts)) {
|
||||||
final JsonObject alertRenderer = alerts.getObject(0).getObject("alertRenderer");
|
final JsonObject alertRenderer = alerts.getObject(0).getObject("alertRenderer");
|
||||||
@ -771,7 +1015,7 @@ public class YoutubeParsingHelper {
|
|||||||
if (alertText != null && alertText.contains("This account has been terminated")) {
|
if (alertText != null && alertText.contains("This account has been terminated")) {
|
||||||
if (alertText.contains("violation") || alertText.contains("violating")
|
if (alertText.contains("violation") || alertText.contains("violating")
|
||||||
|| alertText.contains("infringement")) {
|
|| alertText.contains("infringement")) {
|
||||||
// possible error messages:
|
// Possible error messages:
|
||||||
// "This account has been terminated for a violation of YouTube's Terms of Service."
|
// "This account has been terminated for a violation of YouTube's Terms of Service."
|
||||||
// "This account has been terminated due to multiple or severe violations of YouTube's policy prohibiting hate speech."
|
// "This account has been terminated due to multiple or severe violations of YouTube's policy prohibiting hate speech."
|
||||||
// "This account has been terminated due to multiple or severe violations of YouTube's policy prohibiting content designed to harass, bully or threaten."
|
// "This account has been terminated due to multiple or severe violations of YouTube's policy prohibiting content designed to harass, bully or threaten."
|
||||||
@ -791,7 +1035,8 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static List<MetaInfo> getMetaInfo(final JsonArray contents) throws ParsingException {
|
public static List<MetaInfo> getMetaInfo(@Nonnull final JsonArray contents)
|
||||||
|
throws ParsingException {
|
||||||
final List<MetaInfo> metaInfo = new ArrayList<>();
|
final List<MetaInfo> metaInfo = new ArrayList<>();
|
||||||
for (final Object content : contents) {
|
for (final Object content : contents) {
|
||||||
final JsonObject resultObject = (JsonObject) content;
|
final JsonObject resultObject = (JsonObject) content;
|
||||||
@ -801,10 +1046,12 @@ public class YoutubeParsingHelper {
|
|||||||
|
|
||||||
final JsonObject sectionContent = (JsonObject) sectionContentObject;
|
final JsonObject sectionContent = (JsonObject) sectionContentObject;
|
||||||
if (sectionContent.has("infoPanelContentRenderer")) {
|
if (sectionContent.has("infoPanelContentRenderer")) {
|
||||||
metaInfo.add(getInfoPanelContent(sectionContent.getObject("infoPanelContentRenderer")));
|
metaInfo.add(getInfoPanelContent(sectionContent
|
||||||
|
.getObject("infoPanelContentRenderer")));
|
||||||
}
|
}
|
||||||
if (sectionContent.has("clarificationRenderer")) {
|
if (sectionContent.has("clarificationRenderer")) {
|
||||||
metaInfo.add(getClarificationRendererContent(sectionContent.getObject("clarificationRenderer")
|
metaInfo.add(getClarificationRendererContent(sectionContent
|
||||||
|
.getObject("clarificationRenderer")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,7 +1062,7 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static MetaInfo getInfoPanelContent(final JsonObject infoPanelContentRenderer)
|
private static MetaInfo getInfoPanelContent(@Nonnull final JsonObject infoPanelContentRenderer)
|
||||||
throws ParsingException {
|
throws ParsingException {
|
||||||
final MetaInfo metaInfo = new MetaInfo();
|
final MetaInfo metaInfo = new MetaInfo();
|
||||||
final StringBuilder sb = new StringBuilder();
|
final StringBuilder sb = new StringBuilder();
|
||||||
@ -830,7 +1077,8 @@ public class YoutubeParsingHelper {
|
|||||||
final String metaInfoLinkUrl = YoutubeParsingHelper.getUrlFromNavigationEndpoint(
|
final String metaInfoLinkUrl = YoutubeParsingHelper.getUrlFromNavigationEndpoint(
|
||||||
infoPanelContentRenderer.getObject("sourceEndpoint"));
|
infoPanelContentRenderer.getObject("sourceEndpoint"));
|
||||||
try {
|
try {
|
||||||
metaInfo.addUrl(new URL(Objects.requireNonNull(extractCachedUrlIfNeeded(metaInfoLinkUrl))));
|
metaInfo.addUrl(new URL(Objects.requireNonNull(extractCachedUrlIfNeeded(
|
||||||
|
metaInfoLinkUrl))));
|
||||||
} catch (final NullPointerException | MalformedURLException e) {
|
} catch (final NullPointerException | MalformedURLException e) {
|
||||||
throw new ParsingException("Could not get metadata info URL", e);
|
throw new ParsingException("Could not get metadata info URL", e);
|
||||||
}
|
}
|
||||||
@ -847,12 +1095,14 @@ public class YoutubeParsingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static MetaInfo getClarificationRendererContent(final JsonObject clarificationRenderer)
|
private static MetaInfo getClarificationRendererContent(@Nonnull final JsonObject clarificationRenderer)
|
||||||
throws ParsingException {
|
throws ParsingException {
|
||||||
final MetaInfo metaInfo = new MetaInfo();
|
final MetaInfo metaInfo = new MetaInfo();
|
||||||
|
|
||||||
final String title = YoutubeParsingHelper.getTextFromObject(clarificationRenderer.getObject("contentTitle"));
|
final String title = YoutubeParsingHelper.getTextFromObject(clarificationRenderer
|
||||||
final String text = YoutubeParsingHelper.getTextFromObject(clarificationRenderer.getObject("text"));
|
.getObject("contentTitle"));
|
||||||
|
final String text = YoutubeParsingHelper.getTextFromObject(clarificationRenderer
|
||||||
|
.getObject("text"));
|
||||||
if (title == null || text == null) {
|
if (title == null || text == null) {
|
||||||
throw new ParsingException("Could not extract clarification renderer content");
|
throw new ParsingException("Could not extract clarification renderer content");
|
||||||
}
|
}
|
||||||
@ -863,7 +1113,8 @@ public class YoutubeParsingHelper {
|
|||||||
final JsonObject actionButton = clarificationRenderer.getObject("actionButton")
|
final JsonObject actionButton = clarificationRenderer.getObject("actionButton")
|
||||||
.getObject("buttonRenderer");
|
.getObject("buttonRenderer");
|
||||||
try {
|
try {
|
||||||
final String url = YoutubeParsingHelper.getUrlFromNavigationEndpoint(actionButton.getObject("command"));
|
final String url = YoutubeParsingHelper.getUrlFromNavigationEndpoint(actionButton
|
||||||
|
.getObject("command"));
|
||||||
metaInfo.addUrl(new URL(Objects.requireNonNull(extractCachedUrlIfNeeded(url))));
|
metaInfo.addUrl(new URL(Objects.requireNonNull(extractCachedUrlIfNeeded(url))));
|
||||||
} catch (final NullPointerException | MalformedURLException e) {
|
} catch (final NullPointerException | MalformedURLException e) {
|
||||||
throw new ParsingException("Could not get metadata info URL", e);
|
throw new ParsingException("Could not get metadata info URL", e);
|
||||||
@ -877,15 +1128,18 @@ public class YoutubeParsingHelper {
|
|||||||
metaInfo.addUrlText(metaInfoLinkText);
|
metaInfo.addUrlText(metaInfoLinkText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clarificationRenderer.has("secondaryEndpoint") && clarificationRenderer.has("secondarySource")) {
|
if (clarificationRenderer.has("secondaryEndpoint") && clarificationRenderer
|
||||||
final String url = getUrlFromNavigationEndpoint(clarificationRenderer.getObject("secondaryEndpoint"));
|
.has("secondarySource")) {
|
||||||
// ignore Google URLs, because those point to a Google search about "Covid-19"
|
final String url = getUrlFromNavigationEndpoint(clarificationRenderer
|
||||||
|
.getObject("secondaryEndpoint"));
|
||||||
|
// Ignore Google URLs, because those point to a Google search about "Covid-19"
|
||||||
if (url != null && !isGoogleURL(url)) {
|
if (url != null && !isGoogleURL(url)) {
|
||||||
try {
|
try {
|
||||||
metaInfo.addUrl(new URL(url));
|
metaInfo.addUrl(new URL(url));
|
||||||
final String description = getTextFromObject(clarificationRenderer.getObject("secondarySource"));
|
final String description = getTextFromObject(clarificationRenderer
|
||||||
|
.getObject("secondarySource"));
|
||||||
metaInfo.addUrlText(description == null ? url : description);
|
metaInfo.addUrlText(description == null ? url : description);
|
||||||
} catch (MalformedURLException e) {
|
} catch (final MalformedURLException e) {
|
||||||
throw new ParsingException("Could not get metadata info secondary URL", e);
|
throw new ParsingException("Could not get metadata info secondary URL", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -928,7 +1182,8 @@ public class YoutubeParsingHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String unescapeDocument(final String doc) {
|
@Nonnull
|
||||||
|
public static String unescapeDocument(@Nonnull final String doc) {
|
||||||
return doc
|
return doc
|
||||||
.replaceAll("\\\\x22", "\"")
|
.replaceAll("\\\\x22", "\"")
|
||||||
.replaceAll("\\\\x7b", "{")
|
.replaceAll("\\\\x7b", "{")
|
||||||
@ -936,5 +1191,4 @@ public class YoutubeParsingHelper {
|
|||||||
.replaceAll("\\\\x5b", "[")
|
.replaceAll("\\\\x5b", "[")
|
||||||
.replaceAll("\\\\x5d", "]");
|
.replaceAll("\\\\x5d", "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.youtube;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.utils.JavaScript;
|
import org.schabi.newpipe.extractor.utils.JavaScript;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
import org.schabi.newpipe.extractor.utils.Parser;
|
||||||
|
import org.schabi.newpipe.extractor.utils.StringUtils;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -33,7 +34,10 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public class YoutubeThrottlingDecrypter {
|
public class YoutubeThrottlingDecrypter {
|
||||||
|
|
||||||
private static final String N_PARAM_REGEX = "[&?]n=([^&]+)";
|
private static final Pattern N_PARAM_PATTERN = Pattern.compile("[&?]n=([^&]+)");
|
||||||
|
private static final Pattern FUNCTION_NAME_PATTERN = Pattern.compile(
|
||||||
|
"b=a\\.get\\(\"n\"\\)\\)&&\\(b=(\\w+)\\(b\\),a\\.set\\(\"n\",b\\)");
|
||||||
|
|
||||||
private static final Map<String, String> nParams = new HashMap<>();
|
private static final Map<String, String> nParams = new HashMap<>();
|
||||||
|
|
||||||
private final String functionName;
|
private final String functionName;
|
||||||
@ -62,15 +66,28 @@ public class YoutubeThrottlingDecrypter {
|
|||||||
|
|
||||||
private String parseDecodeFunctionName(final String playerJsCode)
|
private String parseDecodeFunctionName(final String playerJsCode)
|
||||||
throws Parser.RegexException {
|
throws Parser.RegexException {
|
||||||
Pattern pattern = Pattern.compile(
|
return Parser.matchGroup1(FUNCTION_NAME_PATTERN, playerJsCode);
|
||||||
"b=a\\.get\\(\"n\"\\)\\)&&\\(b=(\\w+)\\(b\\),a\\.set\\(\"n\",b\\)");
|
|
||||||
return Parser.matchGroup1(pattern, playerJsCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private String parseDecodeFunction(final String playerJsCode, final String functionName)
|
private String parseDecodeFunction(final String playerJsCode, final String functionName)
|
||||||
throws Parser.RegexException {
|
throws Parser.RegexException {
|
||||||
Pattern functionPattern = Pattern.compile(functionName + "=function(.*?;)\n",
|
try {
|
||||||
|
return parseWithParenthesisMatching(playerJsCode, functionName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return parseWithRegex(playerJsCode, functionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private String parseWithParenthesisMatching(final String playerJsCode, final String functionName) {
|
||||||
|
final String functionBase = functionName + "=function";
|
||||||
|
return functionBase + StringUtils.matchToClosingParenthesis(playerJsCode, functionBase) + ";";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private String parseWithRegex(final String playerJsCode, final String functionName) throws Parser.RegexException {
|
||||||
|
Pattern functionPattern = Pattern.compile(functionName + "=function(.*?}};)\n",
|
||||||
Pattern.DOTALL);
|
Pattern.DOTALL);
|
||||||
return "function " + functionName + Parser.matchGroup1(functionPattern, playerJsCode);
|
return "function " + functionName + Parser.matchGroup1(functionPattern, playerJsCode);
|
||||||
}
|
}
|
||||||
@ -86,12 +103,11 @@ public class YoutubeThrottlingDecrypter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean containsNParam(final String url) {
|
private boolean containsNParam(final String url) {
|
||||||
return Parser.isMatch(N_PARAM_REGEX, url);
|
return Parser.isMatch(N_PARAM_PATTERN, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String parseNParam(final String url) throws Parser.RegexException {
|
private String parseNParam(final String url) throws Parser.RegexException {
|
||||||
Pattern nValuePattern = Pattern.compile(N_PARAM_REGEX);
|
return Parser.matchGroup1(N_PARAM_PATTERN, url);
|
||||||
return Parser.matchGroup1(nValuePattern, url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String decryptNParam(final String nParam) {
|
private String decryptNParam(final String nParam) {
|
||||||
|
@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.StreamingService;
|
|||||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.extractor.downloader.Response;
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
|
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.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
@ -22,15 +23,15 @@ import org.schabi.newpipe.extractor.utils.JsonUtils;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
|
||||||
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.prepareJsonBuilder;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
@ -72,35 +73,110 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
*/
|
*/
|
||||||
private String redirectedChannelId;
|
private String redirectedChannelId;
|
||||||
|
|
||||||
public YoutubeChannelExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
public YoutubeChannelExtractor(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
||||||
String url = super.getUrl() + "/videos?pbj=1&view=0&flow=grid";
|
ExtractionException {
|
||||||
JsonArray ajaxJson = null;
|
final String channelPath = super.getId();
|
||||||
|
final String[] channelId = channelPath.split("/");
|
||||||
|
String id = "";
|
||||||
|
// If the url is an URL which is not a /channel URL, we need to use the
|
||||||
|
// navigation/resolve_url endpoint of the InnerTube API to get the channel id. Otherwise,
|
||||||
|
// we couldn't get information about the channel associated with this URL, if there is one.
|
||||||
|
if (!channelId[0].equals("channel")) {
|
||||||
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
|
getExtractorLocalization(), getExtractorContentCountry())
|
||||||
|
.value("url", "https://www.youtube.com/" + channelPath)
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
int level = 0;
|
final JsonObject jsonResponse = getJsonPostResponse("navigation/resolve_url",
|
||||||
while (level < 3) {
|
body, getExtractorLocalization());
|
||||||
final JsonArray jsonResponse = getJsonResponse(url, getExtractorLocalization());
|
|
||||||
|
|
||||||
final JsonObject endpoint = jsonResponse.getObject(1).getObject("response")
|
if (!isNullOrEmpty(jsonResponse.getObject("error"))) {
|
||||||
.getArray("onResponseReceivedActions").getObject(0).getObject("navigateAction")
|
final JsonObject errorJsonObject = jsonResponse.getObject("error");
|
||||||
.getObject("endpoint");
|
final int errorCode = errorJsonObject.getInt("code");
|
||||||
|
if (errorCode == 404) {
|
||||||
|
throw new ContentNotAvailableException("This channel doesn't exist.");
|
||||||
|
} else {
|
||||||
|
throw new ContentNotAvailableException("Got error:\""
|
||||||
|
+ errorJsonObject.getString("status") + "\": "
|
||||||
|
+ errorJsonObject.getString("message"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final String webPageType = endpoint.getObject("commandMetadata").getObject("webCommandMetadata")
|
final JsonObject endpoint = jsonResponse.getObject("endpoint");
|
||||||
|
|
||||||
|
final String webPageType = endpoint.getObject("commandMetadata")
|
||||||
|
.getObject("webCommandMetadata")
|
||||||
.getString("webPageType", EMPTY_STRING);
|
.getString("webPageType", EMPTY_STRING);
|
||||||
|
|
||||||
final String browseId = endpoint.getObject("browseEndpoint").getString("browseId", EMPTY_STRING);
|
final JsonObject browseEndpoint = endpoint.getObject("browseEndpoint");
|
||||||
|
final String browseId = browseEndpoint.getString("browseId", EMPTY_STRING);
|
||||||
|
|
||||||
if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|
if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|
||||||
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL") && !browseId.isEmpty()) {
|
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL")
|
||||||
|
&& !browseId.isEmpty()) {
|
||||||
if (!browseId.startsWith("UC")) {
|
if (!browseId.startsWith("UC")) {
|
||||||
throw new ExtractionException("Redirected id is not pointing to a channel");
|
throw new ExtractionException("Redirected id is not pointing to a channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
url = "https://www.youtube.com/channel/" + browseId + "/videos?pbj=1&view=0&flow=grid";
|
id = browseId;
|
||||||
|
redirectedChannelId = browseId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = channelId[1];
|
||||||
|
}
|
||||||
|
JsonObject ajaxJson = null;
|
||||||
|
|
||||||
|
int level = 0;
|
||||||
|
while (level < 3) {
|
||||||
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
|
getExtractorLocalization(), getExtractorContentCountry())
|
||||||
|
.value("browseId", id)
|
||||||
|
.value("params", "EgZ2aWRlb3M%3D") // Equal to videos
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
final JsonObject jsonResponse = getJsonPostResponse("browse", body,
|
||||||
|
getExtractorLocalization());
|
||||||
|
|
||||||
|
if (!isNullOrEmpty(jsonResponse.getObject("error"))) {
|
||||||
|
final JsonObject errorJsonObject = jsonResponse.getObject("error");
|
||||||
|
final int errorCode = errorJsonObject.getInt("code");
|
||||||
|
if (errorCode == 404) {
|
||||||
|
throw new ContentNotAvailableException("This channel doesn't exist.");
|
||||||
|
} else {
|
||||||
|
throw new ContentNotAvailableException("Got error:\""
|
||||||
|
+ errorJsonObject.getString("status") + "\": "
|
||||||
|
+ errorJsonObject.getString("message"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject endpoint = jsonResponse.getArray("onResponseReceivedActions")
|
||||||
|
.getObject(0)
|
||||||
|
.getObject("navigateAction")
|
||||||
|
.getObject("endpoint");
|
||||||
|
|
||||||
|
final String webPageType = endpoint.getObject("commandMetadata")
|
||||||
|
.getObject("webCommandMetadata")
|
||||||
|
.getString("webPageType", EMPTY_STRING);
|
||||||
|
|
||||||
|
final String browseId = endpoint.getObject("browseEndpoint").getString("browseId",
|
||||||
|
EMPTY_STRING);
|
||||||
|
|
||||||
|
if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|
||||||
|
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL")
|
||||||
|
&& !browseId.isEmpty()) {
|
||||||
|
if (!browseId.startsWith("UC")) {
|
||||||
|
throw new ExtractionException("Redirected id is not pointing to a channel");
|
||||||
|
}
|
||||||
|
|
||||||
|
id = browseId;
|
||||||
redirectedChannelId = browseId;
|
redirectedChannelId = browseId;
|
||||||
level++;
|
level++;
|
||||||
} else {
|
} else {
|
||||||
@ -113,7 +189,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
throw new ExtractionException("Could not fetch initial JSON data");
|
throw new ExtractionException("Could not fetch initial JSON data");
|
||||||
}
|
}
|
||||||
|
|
||||||
initialData = ajaxJson.getObject(1).getObject("response");
|
initialData = ajaxJson;
|
||||||
YoutubeParsingHelper.defaultAlertsCheck(initialData);
|
YoutubeParsingHelper.defaultAlertsCheck(initialData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +198,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
public String getUrl() throws ParsingException {
|
public String getUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + getId());
|
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + getId());
|
||||||
} catch (ParsingException e) {
|
} catch (final ParsingException e) {
|
||||||
return super.getUrl();
|
return super.getUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +206,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getId() throws ParsingException {
|
public String getId() throws ParsingException {
|
||||||
final String channelId = initialData.getObject("header").getObject("c4TabbedHeaderRenderer")
|
final String channelId = initialData.getObject("header")
|
||||||
|
.getObject("c4TabbedHeaderRenderer")
|
||||||
.getString("channelId", EMPTY_STRING);
|
.getString("channelId", EMPTY_STRING);
|
||||||
|
|
||||||
if (!channelId.isEmpty()) {
|
if (!channelId.isEmpty()) {
|
||||||
@ -146,8 +223,9 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getString("title");
|
return initialData.getObject("header").getObject("c4TabbedHeaderRenderer")
|
||||||
} catch (Exception e) {
|
.getString("title");
|
||||||
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get channel name", e);
|
throw new ParsingException("Could not get channel name", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,11 +233,12 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getAvatarUrl() throws ParsingException {
|
public String getAvatarUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
String url = initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("avatar")
|
String url = initialData.getObject("header")
|
||||||
.getArray("thumbnails").getObject(0).getString("url");
|
.getObject("c4TabbedHeaderRenderer").getObject("avatar").getArray("thumbnails")
|
||||||
|
.getObject(0).getString("url");
|
||||||
|
|
||||||
return fixThumbnailUrl(url);
|
return fixThumbnailUrl(url);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get avatar", e);
|
throw new ParsingException("Could not get avatar", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,15 +246,16 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getBannerUrl() throws ParsingException {
|
public String getBannerUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
String url = initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("banner")
|
String url = initialData.getObject("header")
|
||||||
.getArray("thumbnails").getObject(0).getString("url");
|
.getObject("c4TabbedHeaderRenderer").getObject("banner").getArray("thumbnails")
|
||||||
|
.getObject(0).getString("url");
|
||||||
|
|
||||||
if (url == null || url.contains("s.ytimg.com") || url.contains("default_banner")) {
|
if (url == null || url.contains("s.ytimg.com") || url.contains("default_banner")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fixThumbnailUrl(url);
|
return fixThumbnailUrl(url);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get banner", e);
|
throw new ParsingException("Could not get banner", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,18 +264,20 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
public String getFeedUrl() throws ParsingException {
|
public String getFeedUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return YoutubeParsingHelper.getFeedUrlFrom(getId());
|
return YoutubeParsingHelper.getFeedUrlFrom(getId());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get feed url", e);
|
throw new ParsingException("Could not get feed url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSubscriberCount() throws ParsingException {
|
public long getSubscriberCount() throws ParsingException {
|
||||||
final JsonObject c4TabbedHeaderRenderer = initialData.getObject("header").getObject("c4TabbedHeaderRenderer");
|
final JsonObject c4TabbedHeaderRenderer = initialData.getObject("header")
|
||||||
|
.getObject("c4TabbedHeaderRenderer");
|
||||||
if (c4TabbedHeaderRenderer.has("subscriberCountText")) {
|
if (c4TabbedHeaderRenderer.has("subscriberCountText")) {
|
||||||
try {
|
try {
|
||||||
return Utils.mixedNumberWordToLong(getTextFromObject(c4TabbedHeaderRenderer.getObject("subscriberCountText")));
|
return Utils.mixedNumberWordToLong(getTextFromObject(c4TabbedHeaderRenderer
|
||||||
} catch (NumberFormatException e) {
|
.getObject("subscriberCountText")));
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
throw new ParsingException("Could not get subscriber count", e);
|
throw new ParsingException("Could not get subscriber count", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -206,30 +288,32 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getDescription() throws ParsingException {
|
public String getDescription() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return initialData.getObject("metadata").getObject("channelMetadataRenderer").getString("description");
|
return initialData.getObject("metadata").getObject("channelMetadataRenderer")
|
||||||
} catch (Exception e) {
|
.getString("description");
|
||||||
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get channel description", e);
|
throw new ParsingException("Could not get channel description", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getParentChannelName() throws ParsingException {
|
public String getParentChannelName() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getParentChannelUrl() throws ParsingException {
|
public String getParentChannelUrl() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getParentChannelAvatarUrl() throws ParsingException {
|
public String getParentChannelAvatarUrl() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isVerified() throws ParsingException {
|
public boolean isVerified() throws ParsingException {
|
||||||
final JsonArray badges = initialData.getObject("header").getObject("c4TabbedHeaderRenderer")
|
final JsonArray badges = initialData.getObject("header")
|
||||||
|
.getObject("c4TabbedHeaderRenderer")
|
||||||
.getArray("badges");
|
.getArray("badges");
|
||||||
|
|
||||||
return YoutubeParsingHelper.isVerified(badges);
|
return YoutubeParsingHelper.isVerified(badges);
|
||||||
@ -243,29 +327,36 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
Page nextPage = null;
|
Page nextPage = null;
|
||||||
|
|
||||||
if (getVideoTab() != null) {
|
if (getVideoTab() != null) {
|
||||||
final JsonObject gridRenderer = getVideoTab().getObject("content").getObject("sectionListRenderer")
|
final JsonObject gridRenderer = getVideoTab().getObject("content")
|
||||||
|
.getObject("sectionListRenderer")
|
||||||
.getArray("contents").getObject(0).getObject("itemSectionRenderer")
|
.getArray("contents").getObject(0).getObject("itemSectionRenderer")
|
||||||
.getArray("contents").getObject(0).getObject("gridRenderer");
|
.getArray("contents").getObject(0).getObject("gridRenderer");
|
||||||
|
|
||||||
final JsonObject continuation = collectStreamsFrom(collector, gridRenderer.getArray("items"));
|
final List<String> channelIds = new ArrayList<>();
|
||||||
|
channelIds.add(getName());
|
||||||
|
channelIds.add(getUrl());
|
||||||
|
final JsonObject continuation = collectStreamsFrom(collector, gridRenderer
|
||||||
|
.getArray("items"), channelIds);
|
||||||
|
|
||||||
nextPage = getNextPageFrom(continuation);
|
nextPage = getNextPageFrom(continuation, channelIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, nextPage);
|
return new InfoItemsPage<>(collector, nextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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())) {
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately, we have to fetch the page even if we are only getting next streams,
|
final List<String> channelIds = page.getIds();
|
||||||
// as they don't deliver enough information on their own (the channel name, for example).
|
|
||||||
fetchPage();
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
addClientInfoHeaders(headers);
|
||||||
|
|
||||||
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
|
||||||
final Response response = getDownloader().post(page.getUrl(), null, page.getBody(),
|
final Response response = getDownloader().post(page.getUrl(), null, page.getBody(),
|
||||||
getExtractorLocalization());
|
getExtractorLocalization());
|
||||||
|
|
||||||
@ -275,46 +366,53 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
.getObject(0)
|
.getObject(0)
|
||||||
.getObject("appendContinuationItemsAction");
|
.getObject("appendContinuationItemsAction");
|
||||||
|
|
||||||
final JsonObject continuation = collectStreamsFrom(collector, sectionListContinuation.getArray("continuationItems"));
|
final JsonObject continuation = collectStreamsFrom(collector, sectionListContinuation
|
||||||
|
.getArray("continuationItems"), channelIds);
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, getNextPageFrom(continuation));
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuation, channelIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPageFrom(final JsonObject continuations) throws IOException, ExtractionException {
|
@Nullable
|
||||||
|
private Page getNextPageFrom(final JsonObject continuations,
|
||||||
|
final List<String> channelIds) throws IOException,
|
||||||
|
ExtractionException {
|
||||||
if (isNullOrEmpty(continuations)) {
|
if (isNullOrEmpty(continuations)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject continuationEndpoint = continuations.getObject("continuationEndpoint");
|
final JsonObject continuationEndpoint = continuations.getObject("continuationEndpoint");
|
||||||
final String continuation = continuationEndpoint.getObject("continuationCommand").getString("token");
|
final String continuation = continuationEndpoint.getObject("continuationCommand")
|
||||||
|
.getString("token");
|
||||||
|
|
||||||
final byte[] body = JsonWriter.string(prepareJsonBuilder()
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(getExtractorLocalization(),
|
||||||
|
getExtractorContentCountry())
|
||||||
.value("continuation", continuation)
|
.value("continuation", continuation)
|
||||||
.done())
|
.done())
|
||||||
.getBytes(UTF_8);
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
return new Page("https://www.youtube.com/youtubei/v1/browse?key=" + getKey(),
|
return new Page(YOUTUBEI_V1_URL + "browse?key=" + getKey(), null, channelIds, null, body);
|
||||||
body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect streams from an array of items
|
* Collect streams from an array of items
|
||||||
*
|
*
|
||||||
* @param collector the collector where videos will be commited
|
* @param collector the collector where videos will be committed
|
||||||
* @param videos the array to get videos from
|
* @param videos the array to get videos from
|
||||||
|
* @param channelIds the ids of the channel, which are its name and its URL
|
||||||
* @return the continuation object
|
* @return the continuation object
|
||||||
* @throws ParsingException if an error happened while extracting
|
|
||||||
*/
|
*/
|
||||||
private JsonObject collectStreamsFrom(StreamInfoItemsCollector collector, JsonArray videos) throws ParsingException {
|
private JsonObject collectStreamsFrom(@Nonnull final StreamInfoItemsCollector collector,
|
||||||
|
@Nonnull final JsonArray videos,
|
||||||
|
@Nonnull final List<String> channelIds) {
|
||||||
collector.reset();
|
collector.reset();
|
||||||
|
|
||||||
final String uploaderName = getName();
|
final String uploaderName = channelIds.get(0);
|
||||||
final String uploaderUrl = getUrl();
|
final String uploaderUrl = channelIds.get(1);
|
||||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
|
|
||||||
JsonObject continuation = null;
|
JsonObject continuation = null;
|
||||||
|
|
||||||
for (Object object : videos) {
|
for (final Object object : videos) {
|
||||||
final JsonObject video = (JsonObject) object;
|
final JsonObject video = (JsonObject) object;
|
||||||
if (video.has("gridVideoRenderer")) {
|
if (video.has("gridVideoRenderer")) {
|
||||||
collector.commit(new YoutubeStreamInfoItemExtractor(
|
collector.commit(new YoutubeStreamInfoItemExtractor(
|
||||||
@ -337,16 +435,19 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
|
|||||||
return continuation;
|
return continuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private JsonObject getVideoTab() throws ParsingException {
|
private JsonObject getVideoTab() throws ParsingException {
|
||||||
if (this.videoTab != null) return this.videoTab;
|
if (this.videoTab != null) return this.videoTab;
|
||||||
|
|
||||||
JsonArray tabs = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
|
JsonArray tabs = initialData.getObject("contents")
|
||||||
|
.getObject("twoColumnBrowseResultsRenderer")
|
||||||
.getArray("tabs");
|
.getArray("tabs");
|
||||||
JsonObject videoTab = null;
|
JsonObject videoTab = null;
|
||||||
|
|
||||||
for (Object tab : tabs) {
|
for (final Object tab : tabs) {
|
||||||
if (((JsonObject) tab).has("tabRenderer")) {
|
if (((JsonObject) tab).has("tabRenderer")) {
|
||||||
if (((JsonObject) tab).getObject("tabRenderer").getString("title", EMPTY_STRING).equals("Videos")) {
|
if (((JsonObject) tab).getObject("tabRenderer").getString("title",
|
||||||
|
EMPTY_STRING).equals("Videos")) {
|
||||||
videoTab = ((JsonObject) tab).getObject("tabRenderer");
|
videoTab = ((JsonObject) tab).getObject("tabRenderer");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonArray;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||||
import com.grack.nanojson.JsonObject;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
|
||||||
import com.grack.nanojson.JsonParser;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||||
@ -10,161 +20,237 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
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.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
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.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import com.grack.nanojson.JsonArray;
|
||||||
import java.io.IOException;
|
import com.grack.nanojson.JsonObject;
|
||||||
import java.io.UnsupportedEncodingException;
|
import com.grack.nanojson.JsonWriter;
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
|
|
||||||
public class YoutubeCommentsExtractor extends CommentsExtractor {
|
public class YoutubeCommentsExtractor extends CommentsExtractor {
|
||||||
// using the mobile site for comments because it loads faster and uses get requests instead of post
|
|
||||||
private static final String USER_AGENT = "Mozilla/5.0 (Android 9; Mobile; rv:78.0) Gecko/20100101 Firefox/78.0";
|
|
||||||
private static final Pattern YT_CLIENT_NAME_PATTERN = Pattern.compile("INNERTUBE_CONTEXT_CLIENT_NAME\\\":(.*?)[,}]");
|
|
||||||
|
|
||||||
private String ytClientVersion;
|
private JsonObject nextResponse;
|
||||||
private String ytClientName;
|
|
||||||
private String responseBody;
|
|
||||||
|
|
||||||
public YoutubeCommentsExtractor(StreamingService service, ListLinkHandler uiHandler) {
|
/**
|
||||||
|
* Caching mechanism and holder of the commentsDisabled value.
|
||||||
|
* <br/>
|
||||||
|
* Initial value = empty -> unknown if comments are disabled or not<br/>
|
||||||
|
* Some method calls {@link YoutubeCommentsExtractor#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
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
private Optional<Boolean> optCommentsDisabled = Optional.empty();
|
||||||
|
|
||||||
|
public YoutubeCommentsExtractor(
|
||||||
|
final StreamingService service,
|
||||||
|
final ListLinkHandler uiHandler) {
|
||||||
super(service, uiHandler);
|
super(service, uiHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<CommentsInfoItem> getInitialPage() throws IOException, ExtractionException {
|
public InfoItemsPage<CommentsInfoItem> getInitialPage()
|
||||||
String commentsTokenInside = findValue(responseBody, "sectionListRenderer", "}");
|
throws IOException, ExtractionException {
|
||||||
if (!commentsTokenInside.contains("continuation\":\"")) {
|
|
||||||
commentsTokenInside = findValue(responseBody, "commentSectionRenderer", "}");
|
// Check if findInitialCommentsToken was already called and optCommentsDisabled initialized
|
||||||
|
if (optCommentsDisabled.orElse(false)) {
|
||||||
|
return getInfoItemsPageForDisabledComments();
|
||||||
}
|
}
|
||||||
final String commentsToken = findValue(commentsTokenInside, "continuation\":\"", "\"");
|
|
||||||
|
// Get the token
|
||||||
|
final String commentsToken = findInitialCommentsToken();
|
||||||
|
// Check if the comments have been disabled
|
||||||
|
if (optCommentsDisabled.get()) {
|
||||||
|
return getInfoItemsPageForDisabledComments();
|
||||||
|
}
|
||||||
|
|
||||||
return getPage(getNextPage(commentsToken));
|
return getPage(getNextPage(commentsToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPage(JsonObject ajaxJson) throws ParsingException {
|
/**
|
||||||
final JsonArray arr;
|
* Finds the initial comments token and initializes commentsDisabled.
|
||||||
|
*
|
||||||
|
* @return the continuation token or null if none was found
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private String findInitialCommentsToken() throws ExtractionException {
|
||||||
|
|
||||||
|
final JsonArray jArray = JsonUtils.getArray(nextResponse,
|
||||||
|
"contents.twoColumnWatchNextResults.results.results.contents");
|
||||||
|
|
||||||
|
final Optional<Object> itemSectionRenderer = jArray.stream().filter(o -> {
|
||||||
|
JsonObject jObj = (JsonObject) o;
|
||||||
|
|
||||||
|
if (jObj.has("itemSectionRenderer")) {
|
||||||
try {
|
try {
|
||||||
arr = JsonUtils.getArray(ajaxJson, "response.continuationContents.commentSectionContinuation.continuations");
|
return JsonUtils.getString(jObj, "itemSectionRenderer.targetId")
|
||||||
} catch (Exception e) {
|
.equals("comments-section");
|
||||||
|
} catch (final ParsingException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}).findFirst();
|
||||||
|
|
||||||
|
final String token;
|
||||||
|
|
||||||
|
if (itemSectionRenderer.isPresent()) {
|
||||||
|
token = JsonUtils.getString(((JsonObject) itemSectionRenderer.get())
|
||||||
|
.getObject("itemSectionRenderer").getArray("contents").getObject(0),
|
||||||
|
"continuationItemRenderer.continuationEndpoint.continuationCommand.token");
|
||||||
|
} else {
|
||||||
|
token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
optCommentsDisabled = Optional.of(true);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (arr.isEmpty()) {
|
|
||||||
|
optCommentsDisabled = Optional.of(false);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private InfoItemsPage<CommentsInfoItem> getInfoItemsPageForDisabledComments() {
|
||||||
|
return new InfoItemsPage<>(Collections.emptyList(), null, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Page getNextPage(@Nonnull final JsonObject ajaxJson) throws ExtractionException {
|
||||||
|
final JsonArray jsonArray;
|
||||||
|
final JsonArray onResponseReceivedEndpoints = ajaxJson.getArray(
|
||||||
|
"onResponseReceivedEndpoints");
|
||||||
|
final JsonObject endpoint = onResponseReceivedEndpoints.getObject(
|
||||||
|
onResponseReceivedEndpoints.size() - 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonArray = endpoint.getObject("reloadContinuationItemsCommand", endpoint.getObject(
|
||||||
|
"appendContinuationItemsAction")).getArray("continuationItems");
|
||||||
|
} catch (final Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String continuation;
|
if (jsonArray.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String continuation;
|
||||||
try {
|
try {
|
||||||
continuation = JsonUtils.getString(arr.getObject(0), "nextContinuationData.continuation");
|
continuation = JsonUtils.getString(jsonArray.getObject(jsonArray.size() - 1),
|
||||||
} catch (Exception e) {
|
"continuationItemRenderer.continuationEndpoint.continuationCommand.token");
|
||||||
|
} catch (final Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return getNextPage(continuation);
|
return getNextPage(continuation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPage(String continuation) throws ParsingException {
|
@Nonnull
|
||||||
Map<String, String> params = new HashMap<>();
|
private Page getNextPage(final String continuation) throws ParsingException {
|
||||||
params.put("action_get_comments", "1");
|
return new Page(getUrl(), continuation); // URL is ignored tho
|
||||||
params.put("pbj", "1");
|
|
||||||
params.put("ctoken", continuation);
|
|
||||||
try {
|
|
||||||
return new Page("https://m.youtube.com/watch_comment?" + getDataString(params));
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new ParsingException("Could not get next page url", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<CommentsInfoItem> getPage(final Page page) throws IOException, ExtractionException {
|
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
|
||||||
if (page == null || isNullOrEmpty(page.getUrl())) {
|
throws IOException, ExtractionException {
|
||||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
if (optCommentsDisabled.orElse(false)) {
|
||||||
|
return getInfoItemsPageForDisabledComments();
|
||||||
|
}
|
||||||
|
if (page == null || isNullOrEmpty(page.getId())) {
|
||||||
|
throw new IllegalArgumentException("Page doesn't have the continuation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final String ajaxResponse = makeAjaxRequest(page.getUrl());
|
final Localization localization = getExtractorLocalization();
|
||||||
final JsonObject ajaxJson;
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(localization,
|
||||||
try {
|
getExtractorContentCountry())
|
||||||
ajaxJson = JsonParser.array().from(ajaxResponse).getObject(1);
|
.value("continuation", page.getId())
|
||||||
} catch (Exception e) {
|
.done())
|
||||||
throw new ParsingException("Could not parse json data for comments", e);
|
.getBytes(UTF_8);
|
||||||
}
|
|
||||||
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
|
final JsonObject ajaxJson = getJsonPostResponse("next", body, localization);
|
||||||
|
|
||||||
|
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(
|
||||||
|
getServiceId());
|
||||||
collectCommentsFrom(collector, ajaxJson);
|
collectCommentsFrom(collector, ajaxJson);
|
||||||
return new InfoItemsPage<>(collector, getNextPage(ajaxJson));
|
return new InfoItemsPage<>(collector, getNextPage(ajaxJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectCommentsFrom(CommentsInfoItemsCollector collector, JsonObject ajaxJson) throws ParsingException {
|
private void collectCommentsFrom(final CommentsInfoItemsCollector collector,
|
||||||
JsonArray contents;
|
@Nonnull final JsonObject ajaxJson) throws ParsingException {
|
||||||
try {
|
|
||||||
contents = JsonUtils.getArray(ajaxJson, "response.continuationContents.commentSectionContinuation.items");
|
final JsonArray onResponseReceivedEndpoints = ajaxJson.getArray(
|
||||||
} catch (Exception e) {
|
"onResponseReceivedEndpoints");
|
||||||
//no comments
|
final JsonObject commentsEndpoint = onResponseReceivedEndpoints.getObject(
|
||||||
|
onResponseReceivedEndpoints.size() - 1);
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
if (commentsEndpoint.has("reloadContinuationItemsCommand")) {
|
||||||
|
path = "reloadContinuationItemsCommand.continuationItems";
|
||||||
|
} else if (commentsEndpoint.has("appendContinuationItemsAction")) {
|
||||||
|
path = "appendContinuationItemsAction.continuationItems";
|
||||||
|
} else {
|
||||||
|
// No comments
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<Object> comments;
|
|
||||||
|
final JsonArray contents;
|
||||||
try {
|
try {
|
||||||
comments = JsonUtils.getValues(contents, "commentThreadRenderer.comment.commentRenderer");
|
contents = (JsonArray) JsonUtils.getArray(commentsEndpoint, path).clone();
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("unable to get parse youtube comments", e);
|
// No comments
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Object c : comments) {
|
final int index = contents.size() - 1;
|
||||||
|
if (contents.getObject(index).has("continuationItemRenderer")) {
|
||||||
|
contents.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Object> comments;
|
||||||
|
try {
|
||||||
|
comments = JsonUtils.getValues(contents,
|
||||||
|
"commentThreadRenderer.comment.commentRenderer");
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new ParsingException("Unable to get parse youtube comments", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Object c : comments) {
|
||||||
if (c instanceof JsonObject) {
|
if (c instanceof JsonObject) {
|
||||||
CommentsInfoItemExtractor extractor = new YoutubeCommentsInfoItemExtractor((JsonObject) c, getUrl(), getTimeAgoParser());
|
final CommentsInfoItemExtractor extractor = new YoutubeCommentsInfoItemExtractor(
|
||||||
|
(JsonObject) c, getUrl(), getTimeAgoParser());
|
||||||
collector.commit(extractor);
|
collector.commit(extractor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
final Map<String, List<String>> requestHeaders = new HashMap<>();
|
throws IOException, ExtractionException {
|
||||||
requestHeaders.put("User-Agent", singletonList(USER_AGENT));
|
final Localization localization = getExtractorLocalization();
|
||||||
final Response response = downloader.get(getUrl(), requestHeaders, getExtractorLocalization());
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(localization,
|
||||||
responseBody = YoutubeParsingHelper.unescapeDocument(response.responseBody());
|
getExtractorContentCountry())
|
||||||
ytClientVersion = findValue(responseBody, "INNERTUBE_CONTEXT_CLIENT_VERSION\":\"", "\"");
|
.value("videoId", getId())
|
||||||
ytClientName = Parser.matchGroup1(YT_CLIENT_NAME_PATTERN, responseBody);
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
nextResponse = getJsonPostResponse("next", body, localization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private String makeAjaxRequest(String siteUrl) throws IOException, ReCaptchaException {
|
@Override
|
||||||
Map<String, List<String>> requestHeaders = new HashMap<>();
|
public boolean isCommentsDisabled() throws ExtractionException {
|
||||||
requestHeaders.put("Accept", singletonList("*/*"));
|
// Check if commentsDisabled has to be initialized
|
||||||
requestHeaders.put("User-Agent", singletonList(USER_AGENT));
|
if (!optCommentsDisabled.isPresent()) {
|
||||||
requestHeaders.put("X-YouTube-Client-Version", singletonList(ytClientVersion));
|
// Initialize commentsDisabled
|
||||||
requestHeaders.put("X-YouTube-Client-Name", singletonList(ytClientName));
|
this.findInitialCommentsToken();
|
||||||
return getDownloader().get(siteUrl, requestHeaders, getExtractorLocalization()).responseBody();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getDataString(Map<String, String> params) throws UnsupportedEncodingException {
|
return optCommentsDisabled.get();
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
|
||||||
if (first)
|
|
||||||
first = false;
|
|
||||||
else
|
|
||||||
result.append("&");
|
|
||||||
result.append(URLEncoder.encode(entry.getKey(), UTF_8));
|
|
||||||
result.append("=");
|
|
||||||
result.append(URLEncoder.encode(entry.getValue(), UTF_8));
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findValue(final String doc, final String start, final String end) {
|
|
||||||
final int beginIndex = doc.indexOf(start) + start.length();
|
|
||||||
final int endIndex = doc.indexOf(end, beginIndex);
|
|
||||||
return doc.substring(beginIndex, endIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,9 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
private final String url;
|
private final String url;
|
||||||
private final TimeAgoParser timeAgoParser;
|
private final TimeAgoParser timeAgoParser;
|
||||||
|
|
||||||
public YoutubeCommentsInfoItemExtractor(JsonObject json, String url, TimeAgoParser timeAgoParser) {
|
public YoutubeCommentsInfoItemExtractor(final JsonObject json,
|
||||||
|
final String url,
|
||||||
|
final TimeAgoParser timeAgoParser) {
|
||||||
this.json = json;
|
this.json = json;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.timeAgoParser = timeAgoParser;
|
this.timeAgoParser = timeAgoParser;
|
||||||
@ -37,7 +39,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
try {
|
try {
|
||||||
final JsonArray arr = JsonUtils.getArray(json, "authorThumbnail.thumbnails");
|
final JsonArray arr = JsonUtils.getArray(json, "authorThumbnail.thumbnails");
|
||||||
return JsonUtils.getString(arr.getObject(2), "url");
|
return JsonUtils.getString(arr.getObject(2), "url");
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get thumbnail url", e);
|
throw new ParsingException("Could not get thumbnail url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,7 +48,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return getTextFromObject(JsonUtils.getObject(json, "authorText"));
|
return getTextFromObject(JsonUtils.getObject(json, "authorText"));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
return EMPTY_STRING;
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,7 +57,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
public String getTextualUploadDate() throws ParsingException {
|
public String getTextualUploadDate() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return getTextFromObject(JsonUtils.getObject(json, "publishedTimeText"));
|
return getTextFromObject(JsonUtils.getObject(json, "publishedTimeText"));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get publishedTimeText", e);
|
throw new ParsingException("Could not get publishedTimeText", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +66,8 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
@Override
|
@Override
|
||||||
public DateWrapper getUploadDate() throws ParsingException {
|
public DateWrapper getUploadDate() throws ParsingException {
|
||||||
String textualPublishedTime = getTextualUploadDate();
|
String textualPublishedTime = getTextualUploadDate();
|
||||||
if (timeAgoParser != null && textualPublishedTime != null && !textualPublishedTime.isEmpty()) {
|
if (timeAgoParser != null && textualPublishedTime != null
|
||||||
|
&& !textualPublishedTime.isEmpty()) {
|
||||||
return timeAgoParser.parse(textualPublishedTime);
|
return timeAgoParser.parse(textualPublishedTime);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@ -72,23 +75,29 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implNote The method is parsing internally a localized string.<br>
|
* @implNote The method tries first to get the exact like count by using the accessibility data
|
||||||
|
* returned. But if the parsing of this accessibility data fails, the method parses internally
|
||||||
|
* a localized string.
|
||||||
|
* <br>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>
|
* <li>More than 1k likes will result in an inaccurate number</li>
|
||||||
* More than 1k likes will result in an inaccurate number
|
* <li>This will fail for other languages than English. However as long as the Extractor
|
||||||
* </li>
|
* only uses "en-GB" (as seen in {@link
|
||||||
* <li>
|
* org.schabi.newpipe.extractor.services.youtube.YoutubeService#getSupportedLocalizations})
|
||||||
* This will fail for other languages than English.
|
* , everything will work fine.</li>
|
||||||
* However as long as the Extractor only uses "en-GB"
|
|
||||||
* (as seen in {@link org.schabi.newpipe.extractor.services.youtube.YoutubeService#SUPPORTED_LANGUAGES})
|
|
||||||
* everything will work fine.
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
* </ul>
|
||||||
* <br>
|
* <br>
|
||||||
* Consider using {@link #getTextualLikeCount()}
|
* Consider using {@link #getTextualLikeCount()}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int getLikeCount() throws ParsingException {
|
public int getLikeCount() throws ParsingException {
|
||||||
|
// Try first to get the exact like count by using the accessibility data
|
||||||
|
final String likeCount;
|
||||||
|
try {
|
||||||
|
likeCount = Utils.removeNonDigitCharacters(JsonUtils.getString(json,
|
||||||
|
"actionButtons.commentActionButtonsRenderer.likeButton.toggleButtonRenderer.accessibilityData.accessibilityData.label"));
|
||||||
|
} catch (final Exception e) {
|
||||||
|
// Use the approximate like count returned into the voteCount object
|
||||||
// This may return a language dependent version, e.g. in German: 3,3 Mio
|
// This may return a language dependent version, e.g. in German: 3,3 Mio
|
||||||
final String textualLikeCount = getTextualLikeCount();
|
final String textualLikeCount = getTextualLikeCount();
|
||||||
try {
|
try {
|
||||||
@ -97,8 +106,20 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (int) Utils.mixedNumberWordToLong(textualLikeCount);
|
return (int) Utils.mixedNumberWordToLong(textualLikeCount);
|
||||||
} catch (Exception e) {
|
} catch (final Exception i) {
|
||||||
throw new ParsingException("Unexpected error while converting textual like count to like count", e);
|
throw new ParsingException(
|
||||||
|
"Unexpected error while converting textual like count to like count", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Utils.isBlank(likeCount)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Integer.parseInt(likeCount);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new ParsingException("Unexpected error while parsing like count as Integer", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,8 +154,8 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
return EMPTY_STRING;
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
return getTextFromObject(voteCountObj);
|
return getTextFromObject(voteCountObj);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get vote count", e);
|
throw new ParsingException("Could not get the vote count", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,9 +169,10 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
return EMPTY_STRING;
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
final String commentText = getTextFromObject(contentText);
|
final String commentText = getTextFromObject(contentText);
|
||||||
// youtube adds U+FEFF in some comments. eg. https://www.youtube.com/watch?v=Nj4F63E59io<feff>
|
// YouTube adds U+FEFF in some comments.
|
||||||
|
// eg. https://www.youtube.com/watch?v=Nj4F63E59io<feff>
|
||||||
return Utils.removeUTF8BOM(commentText);
|
return Utils.removeUTF8BOM(commentText);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get comment text", e);
|
throw new ParsingException("Could not get comment text", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +181,7 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
public String getCommentId() throws ParsingException {
|
public String getCommentId() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return JsonUtils.getString(json, "commentId");
|
return JsonUtils.getString(json, "commentId");
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get comment id", e);
|
throw new ParsingException("Could not get comment id", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,14 +191,16 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
try {
|
try {
|
||||||
JsonArray arr = JsonUtils.getArray(json, "authorThumbnail.thumbnails");
|
JsonArray arr = JsonUtils.getArray(json, "authorThumbnail.thumbnails");
|
||||||
return JsonUtils.getString(arr.getObject(2), "url");
|
return JsonUtils.getString(arr.getObject(2), "url");
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get author thumbnail", e);
|
throw new ParsingException("Could not get author thumbnail", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isHeartedByUploader() throws ParsingException {
|
public boolean isHeartedByUploader() throws ParsingException {
|
||||||
return json.has("creatorHeart");
|
final JsonObject commentActionButtonsRenderer = json.getObject("actionButtons")
|
||||||
|
.getObject("commentActionButtonsRenderer");
|
||||||
|
return commentActionButtonsRenderer.has("creatorHeart");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -185,15 +209,14 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUploaderVerified() {
|
public boolean isUploaderVerified() {
|
||||||
// impossible to get this information from the mobile layout
|
return json.has("authorCommentBadge");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderName() throws ParsingException {
|
public String getUploaderName() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return getTextFromObject(JsonUtils.getObject(json, "authorText"));
|
return getTextFromObject(JsonUtils.getObject(json, "authorText"));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
return EMPTY_STRING;
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,10 +224,10 @@ public class YoutubeCommentsInfoItemExtractor implements CommentsInfoItemExtract
|
|||||||
@Override
|
@Override
|
||||||
public String getUploaderUrl() throws ParsingException {
|
public String getUploaderUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return "https://youtube.com/channel/" + JsonUtils.getString(json, "authorEndpoint.browseEndpoint.browseId");
|
return "https://www.youtube.com/channel/" + JsonUtils.getString(json,
|
||||||
} catch (Exception e) {
|
"authorEndpoint.browseEndpoint.browseId");
|
||||||
|
} catch (final Exception e) {
|
||||||
return EMPTY_STRING;
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonArray;
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonBuilder;
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
@ -11,6 +13,7 @@ import org.schabi.newpipe.extractor.downloader.Response;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
@ -19,19 +22,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
|||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.extractCookieValue;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
import static org.schabi.newpipe.extractor.utils.Utils.*;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getResponse;
|
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link YoutubePlaylistExtractor} for a mix (auto-generated playlist).
|
* A {@link YoutubePlaylistExtractor} for a mix (auto-generated playlist).
|
||||||
@ -58,12 +56,34 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
final String url = getUrl() + "&pbj=1";
|
final Localization localization = getExtractorLocalization();
|
||||||
final Response response = getResponse(url, getExtractorLocalization());
|
final URL url = stringToURL(getUrl());
|
||||||
final JsonArray ajaxJson = JsonUtils.toJsonArray(response.responseBody());
|
final String mixPlaylistId = getId();
|
||||||
initialData = ajaxJson.getObject(3).getObject("response");
|
final String videoId = getQueryValue(url, "v");
|
||||||
|
final String playlistIndexString = getQueryValue(url, "index");
|
||||||
|
|
||||||
|
final JsonBuilder<JsonObject> jsonBody = prepareDesktopJsonBuilder(localization,
|
||||||
|
getExtractorContentCountry()).value("playlistId", mixPlaylistId);
|
||||||
|
if (videoId != null) {
|
||||||
|
jsonBody.value("videoId", videoId);
|
||||||
|
}
|
||||||
|
if (playlistIndexString != null) {
|
||||||
|
jsonBody.value("playlistIndex", Integer.parseInt(playlistIndexString));
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] body = JsonWriter.string(jsonBody.done()).getBytes(UTF_8);
|
||||||
|
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
addClientInfoHeaders(headers);
|
||||||
|
|
||||||
|
final Response response = getDownloader().post(YOUTUBEI_V1_URL + "next?key=" + getKey(),
|
||||||
|
headers, body, localization);
|
||||||
|
|
||||||
|
initialData = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||||
playlistData = initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
playlistData = initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
||||||
.getObject("playlist").getObject("playlist");
|
.getObject("playlist").getObject("playlist");
|
||||||
|
if (isNullOrEmpty(playlistData)) throw new ExtractionException(
|
||||||
|
"Could not get playlistData");
|
||||||
cookieValue = extractCookieValue(COOKIE_NAME, response);
|
cookieValue = extractCookieValue(COOKIE_NAME, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,10 +103,9 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||||||
return getThumbnailUrlFromPlaylistId(playlistData.getString("playlistId"));
|
return getThumbnailUrlFromPlaylistId(playlistData.getString("playlistId"));
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
try {
|
try {
|
||||||
//fallback to thumbnail of current video. Always the case for channel mix
|
// Fallback to thumbnail of current video. Always the case for channel mix
|
||||||
return getThumbnailUrlFromVideoId(
|
return getThumbnailUrlFromVideoId(initialData.getObject("currentVideoEndpoint")
|
||||||
initialData.getObject("currentVideoEndpoint").getObject("watchEndpoint")
|
.getObject("watchEndpoint").getString("videoId"));
|
||||||
.getString("videoId"));
|
|
||||||
} catch (final Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
}
|
}
|
||||||
throw new ParsingException("Could not get playlist thumbnail", e);
|
throw new ParsingException("Could not get playlist thumbnail", e);
|
||||||
@ -100,19 +119,19 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderUrl() {
|
public String getUploaderUrl() {
|
||||||
//Youtube mix are auto-generated
|
// YouTube mixes are auto-generated by YouTube
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderName() {
|
public String getUploaderName() {
|
||||||
//Youtube mix are auto-generated by YouTube
|
// YouTube mixes are auto-generated by YouTube
|
||||||
return "YouTube";
|
return "YouTube";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderAvatarUrl() {
|
public String getUploaderAvatarUrl() {
|
||||||
//Youtube mix are auto-generated by YouTube
|
// YouTube mixes are auto-generated by YouTube
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,63 +142,80 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getStreamCount() {
|
public long getStreamCount() {
|
||||||
// Auto-generated playlist always start with 25 videos and are endless
|
// Auto-generated playlists always start with 25 videos and are endless
|
||||||
return ListExtractor.ITEM_COUNT_INFINITE;
|
return ListExtractor.ITEM_COUNT_INFINITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
|
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException,
|
||||||
|
ExtractionException {
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
collectStreamsFrom(collector, playlistData.getArray("contents"));
|
collectStreamsFrom(collector, playlistData.getArray("contents"));
|
||||||
|
|
||||||
final Map<String, String> cookies = new HashMap<>();
|
final Map<String, String> cookies = new HashMap<>();
|
||||||
cookies.put(COOKIE_NAME, cookieValue);
|
cookies.put(COOKIE_NAME, cookieValue);
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, new Page(getNextPageUrlFrom(playlistData), cookies));
|
return new InfoItemsPage<>(collector, getNextPageFrom(playlistData, cookies));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getNextPageUrlFrom(final JsonObject playlistJson) throws ExtractionException {
|
private Page getNextPageFrom(final JsonObject playlistJson,
|
||||||
|
final Map<String, String> cookies) throws IOException,
|
||||||
|
ExtractionException {
|
||||||
final JsonObject lastStream = ((JsonObject) playlistJson.getArray("contents")
|
final JsonObject lastStream = ((JsonObject) playlistJson.getArray("contents")
|
||||||
.get(playlistJson.getArray("contents").size() - 1));
|
.get(playlistJson.getArray("contents").size() - 1));
|
||||||
if (lastStream == null || lastStream.getObject("playlistPanelVideoRenderer") == null) {
|
if (lastStream == null || lastStream.getObject("playlistPanelVideoRenderer") == null) {
|
||||||
throw new ExtractionException("Could not extract next page url");
|
throw new ExtractionException("Could not extract next page url");
|
||||||
}
|
}
|
||||||
|
|
||||||
return getUrlFromNavigationEndpoint(
|
final JsonObject watchEndpoint = lastStream.getObject("playlistPanelVideoRenderer")
|
||||||
lastStream.getObject("playlistPanelVideoRenderer").getObject("navigationEndpoint"))
|
.getObject("navigationEndpoint").getObject("watchEndpoint");
|
||||||
+ "&pbj=1";
|
final String playlistId = watchEndpoint.getString("playlistId");
|
||||||
|
final String videoId = watchEndpoint.getString("videoId");
|
||||||
|
final int index = watchEndpoint.getInt("index");
|
||||||
|
final String params = watchEndpoint.getString("params");
|
||||||
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(getExtractorLocalization(),
|
||||||
|
getExtractorContentCountry())
|
||||||
|
.value("videoId", videoId)
|
||||||
|
.value("playlistId", playlistId)
|
||||||
|
.value("playlistIndex", index)
|
||||||
|
.value("params", params)
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
return new Page(YOUTUBEI_V1_URL + "next?key=" + getKey(), null, null, cookies, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page)
|
public InfoItemsPage<StreamInfoItem> getPage(final Page page) throws IOException,
|
||||||
throws ExtractionException, IOException {
|
ExtractionException {
|
||||||
if (page == null || isNullOrEmpty(page.getUrl())) {
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||||
throw new IllegalArgumentException("Page url is empty or null");
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||||
}
|
}
|
||||||
if (!page.getCookies().containsKey(COOKIE_NAME)) {
|
if (!page.getCookies().containsKey(COOKIE_NAME)) {
|
||||||
throw new IllegalArgumentException("Cooke '" + COOKIE_NAME + "' is missing");
|
throw new IllegalArgumentException("Cookie '" + COOKIE_NAME + "' is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonArray ajaxJson = getJsonResponse(page, getExtractorLocalization());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
final JsonObject playlistJson =
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
ajaxJson.getObject(3).getObject("response").getObject("contents")
|
addClientInfoHeaders(headers);
|
||||||
.getObject("twoColumnWatchNextResults").getObject("playlist")
|
|
||||||
.getObject("playlist");
|
final Response response = getDownloader().post(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");
|
||||||
final JsonArray allStreams = playlistJson.getArray("contents");
|
final JsonArray allStreams = playlistJson.getArray("contents");
|
||||||
// Sublist because youtube returns up to 24 previous streams in the mix
|
// Sublist because YouTube returns up to 24 previous streams in the mix
|
||||||
// +1 because the stream of "currentIndex" was already extracted in previous request
|
// +1 because the stream of "currentIndex" was already extracted in previous request
|
||||||
final List<Object> newStreams =
|
final List<Object> newStreams =
|
||||||
allStreams.subList(playlistJson.getInt("currentIndex") + 1, allStreams.size());
|
allStreams.subList(playlistJson.getInt("currentIndex") + 1, allStreams.size());
|
||||||
|
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
|
||||||
collectStreamsFrom(collector, newStreams);
|
collectStreamsFrom(collector, newStreams);
|
||||||
return new InfoItemsPage<>(collector,
|
return new InfoItemsPage<>(collector, getNextPageFrom(playlistJson, page.getCookies()));
|
||||||
new Page(getNextPageUrlFrom(playlistJson), page.getCookies()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectStreamsFrom(
|
private void collectStreamsFrom(@Nonnull final StreamInfoItemsCollector collector,
|
||||||
@Nonnull final StreamInfoItemsCollector collector,
|
|
||||||
@Nullable final List<Object> streams) {
|
@Nullable final List<Object> streams) {
|
||||||
|
|
||||||
if (streams == null) {
|
if (streams == null) {
|
||||||
@ -193,7 +229,8 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||||||
final JsonObject streamInfo = ((JsonObject) stream)
|
final JsonObject streamInfo = ((JsonObject) stream)
|
||||||
.getObject("playlistPanelVideoRenderer");
|
.getObject("playlistPanelVideoRenderer");
|
||||||
if (streamInfo != null) {
|
if (streamInfo != null) {
|
||||||
collector.commit(new YoutubeStreamInfoItemExtractor(streamInfo, timeAgoParser));
|
collector.commit(new YoutubeStreamInfoItemExtractor(streamInfo,
|
||||||
|
timeAgoParser));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +241,7 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
|||||||
if (playlistId.startsWith("RDMM")) {
|
if (playlistId.startsWith("RDMM")) {
|
||||||
videoId = playlistId.substring(4);
|
videoId = playlistId.substring(4);
|
||||||
} else if (playlistId.startsWith("RDCMUC")) {
|
} else if (playlistId.startsWith("RDCMUC")) {
|
||||||
throw new ParsingException("is channel mix");
|
throw new ParsingException("This playlist is a channel mix");
|
||||||
} else {
|
} else {
|
||||||
videoId = playlistId.substring(2);
|
videoId = playlistId.substring(2);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import org.schabi.newpipe.extractor.utils.Parser;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -33,15 +34,18 @@ import static org.schabi.newpipe.extractor.utils.Utils.*;
|
|||||||
public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
||||||
private JsonObject initialData;
|
private JsonObject initialData;
|
||||||
|
|
||||||
public YoutubeMusicSearchExtractor(final StreamingService service, final SearchQueryHandler linkHandler) {
|
public YoutubeMusicSearchExtractor(final StreamingService service,
|
||||||
|
final SearchQueryHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
final String[] youtubeMusicKeys = YoutubeParsingHelper.getYoutubeMusicKeys();
|
throws IOException, ExtractionException {
|
||||||
|
final String[] youtubeMusicKeys = YoutubeParsingHelper.getYoutubeMusicKey();
|
||||||
|
|
||||||
final String url = "https://music.youtube.com/youtubei/v1/search?alt=json&key=" + youtubeMusicKeys[0];
|
final String url = "https://music.youtube.com/youtubei/v1/search?alt=json&key="
|
||||||
|
+ youtubeMusicKeys[0];
|
||||||
|
|
||||||
final String params;
|
final String params;
|
||||||
|
|
||||||
@ -67,17 +71,16 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
byte[] json = JsonWriter.string()
|
final byte[] json = JsonWriter.string()
|
||||||
.object()
|
.object()
|
||||||
.object("context")
|
.object("context")
|
||||||
.object("client")
|
.object("client")
|
||||||
.value("clientName", "WEB_REMIX")
|
.value("clientName", "WEB_REMIX")
|
||||||
.value("clientVersion", youtubeMusicKeys[2])
|
.value("clientVersion", youtubeMusicKeys[2])
|
||||||
.value("hl", "en")
|
.value("hl", "en-GB")
|
||||||
.value("gl", getExtractorContentCountry().getCountryCode())
|
.value("gl", getExtractorContentCountry().getCountryCode())
|
||||||
.array("experimentIds").end()
|
.array("experimentIds").end()
|
||||||
.value("experimentsToken", "")
|
.value("experimentsToken", EMPTY_STRING)
|
||||||
.value("utcOffsetMinutes", 0)
|
|
||||||
.object("locationInfo").end()
|
.object("locationInfo").end()
|
||||||
.object("musicAppInfo").end()
|
.object("musicAppInfo").end()
|
||||||
.end()
|
.end()
|
||||||
@ -88,6 +91,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
.end()
|
.end()
|
||||||
.object("activePlayers").end()
|
.object("activePlayers").end()
|
||||||
.object("user")
|
.object("user")
|
||||||
|
// TO DO: provide a way to enable restricted mode with:
|
||||||
.value("enableSafetyMode", false)
|
.value("enableSafetyMode", false)
|
||||||
.end()
|
.end()
|
||||||
.end()
|
.end()
|
||||||
@ -103,11 +107,12 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
||||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||||
|
|
||||||
final String responseBody = getValidJsonResponseBody(getDownloader().post(url, headers, json));
|
final String responseBody = getValidJsonResponseBody(getDownloader().post(url, headers,
|
||||||
|
json));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initialData = JsonParser.object().from(responseBody);
|
initialData = JsonParser.object().from(responseBody);
|
||||||
} catch (JsonParserException e) {
|
} catch (final JsonParserException e) {
|
||||||
throw new ParsingException("Could not parse JSON", e);
|
throw new ParsingException("Could not parse JSON", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,37 +126,46 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getSearchSuggestion() throws ParsingException {
|
public String getSearchSuggestion() throws ParsingException {
|
||||||
final JsonObject itemSectionRenderer = initialData.getObject("contents").getObject("sectionListRenderer")
|
final JsonObject itemSectionRenderer = JsonUtils.getArray(JsonUtils.getArray(initialData,
|
||||||
.getArray("contents").getObject(0).getObject("itemSectionRenderer");
|
"contents.tabbedSearchResultsRenderer.tabs").getObject(0),
|
||||||
|
"tabRenderer.content.sectionListRenderer.contents")
|
||||||
|
.getObject(0)
|
||||||
|
.getObject("itemSectionRenderer");
|
||||||
if (itemSectionRenderer.isEmpty()) {
|
if (itemSectionRenderer.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents")
|
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents")
|
||||||
.getObject(0).getObject("didYouMeanRenderer");
|
.getObject(0).getObject("didYouMeanRenderer");
|
||||||
final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents").getObject(0)
|
final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents")
|
||||||
|
.getObject(0)
|
||||||
.getObject("showingResultsForRenderer");
|
.getObject("showingResultsForRenderer");
|
||||||
|
|
||||||
if (!didYouMeanRenderer.isEmpty()) {
|
if (!didYouMeanRenderer.isEmpty()) {
|
||||||
return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery"));
|
return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery"));
|
||||||
} else if (!showingResultsForRenderer.isEmpty()) {
|
} else if (!showingResultsForRenderer.isEmpty()) {
|
||||||
return JsonUtils.getString(showingResultsForRenderer, "correctedQueryEndpoint.searchEndpoint.query");
|
return JsonUtils.getString(showingResultsForRenderer,
|
||||||
|
"correctedQueryEndpoint.searchEndpoint.query");
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCorrectedSearch() {
|
public boolean isCorrectedSearch() throws ParsingException {
|
||||||
final JsonObject itemSectionRenderer = initialData.getObject("contents").getObject("sectionListRenderer")
|
final JsonObject itemSectionRenderer = JsonUtils.getArray(JsonUtils.getArray(initialData,
|
||||||
.getArray("contents").getObject(0).getObject("itemSectionRenderer");
|
"contents.tabbedSearchResultsRenderer.tabs").getObject(0),
|
||||||
|
"tabRenderer.content.sectionListRenderer.contents")
|
||||||
|
.getObject(0)
|
||||||
|
.getObject("itemSectionRenderer");
|
||||||
if (itemSectionRenderer.isEmpty()) {
|
if (itemSectionRenderer.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents").getObject(0)
|
JsonObject firstContent = itemSectionRenderer.getArray("contents").getObject(0);
|
||||||
.getObject("showingResultsForRenderer");
|
|
||||||
return !showingResultsForRenderer.isEmpty();
|
return firstContent.has("didYouMeanRenderer")
|
||||||
|
|| firstContent.has("showingResultsForRenderer");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@ -162,16 +176,19 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<InfoItem> getInitialPage() throws ExtractionException, IOException {
|
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||||
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
||||||
|
|
||||||
final JsonArray contents = initialData.getObject("contents").getObject("sectionListRenderer").getArray("contents");
|
final JsonArray contents = JsonUtils.getArray(JsonUtils.getArray(initialData,
|
||||||
|
"contents.tabbedSearchResultsRenderer.tabs").getObject(0),
|
||||||
|
"tabRenderer.content.sectionListRenderer.contents");
|
||||||
|
|
||||||
Page nextPage = null;
|
Page nextPage = null;
|
||||||
|
|
||||||
for (Object content : contents) {
|
for (final Object content : contents) {
|
||||||
if (((JsonObject) content).has("musicShelfRenderer")) {
|
if (((JsonObject) content).has("musicShelfRenderer")) {
|
||||||
final JsonObject musicShelfRenderer = ((JsonObject) content).getObject("musicShelfRenderer");
|
final JsonObject musicShelfRenderer = ((JsonObject) content)
|
||||||
|
.getObject("musicShelfRenderer");
|
||||||
|
|
||||||
collectMusicStreamsFrom(collector, musicShelfRenderer.getArray("contents"));
|
collectMusicStreamsFrom(collector, musicShelfRenderer.getArray("contents"));
|
||||||
|
|
||||||
@ -183,14 +200,15 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<InfoItem> getPage(final Page page) throws IOException, ExtractionException {
|
public InfoItemsPage<InfoItem> getPage(final Page page)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
if (page == null || isNullOrEmpty(page.getUrl())) {
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
||||||
|
|
||||||
final String[] youtubeMusicKeys = YoutubeParsingHelper.getYoutubeMusicKeys();
|
final String[] youtubeMusicKeys = YoutubeParsingHelper.getYoutubeMusicKey();
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
byte[] json = JsonWriter.string()
|
byte[] json = JsonWriter.string()
|
||||||
@ -227,16 +245,18 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
headers.put("Referer", Collections.singletonList("music.youtube.com"));
|
||||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||||
|
|
||||||
final String responseBody = getValidJsonResponseBody(getDownloader().post(page.getUrl(), headers, json));
|
final String responseBody = getValidJsonResponseBody(getDownloader().post(page.getUrl(),
|
||||||
|
headers, json));
|
||||||
|
|
||||||
final JsonObject ajaxJson;
|
final JsonObject ajaxJson;
|
||||||
try {
|
try {
|
||||||
ajaxJson = JsonParser.object().from(responseBody);
|
ajaxJson = JsonParser.object().from(responseBody);
|
||||||
} catch (JsonParserException e) {
|
} catch (final JsonParserException e) {
|
||||||
throw new ParsingException("Could not parse JSON", e);
|
throw new ParsingException("Could not parse JSON", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject musicShelfContinuation = ajaxJson.getObject("continuationContents").getObject("musicShelfContinuation");
|
final JsonObject musicShelfContinuation = ajaxJson.getObject("continuationContents")
|
||||||
|
.getObject("musicShelfContinuation");
|
||||||
|
|
||||||
collectMusicStreamsFrom(collector, musicShelfContinuation.getArray("contents"));
|
collectMusicStreamsFrom(collector, musicShelfContinuation.getArray("contents"));
|
||||||
final JsonArray continuations = musicShelfContinuation.getArray("continuations");
|
final JsonArray continuations = musicShelfContinuation.getArray("continuations");
|
||||||
@ -244,31 +264,32 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
return new InfoItemsPage<>(collector, getNextPageFrom(continuations));
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuations));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectMusicStreamsFrom(final InfoItemsSearchCollector collector, final JsonArray videos) {
|
private void collectMusicStreamsFrom(final InfoItemsSearchCollector collector,
|
||||||
|
@Nonnull final JsonArray videos) {
|
||||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
|
|
||||||
for (Object item : videos) {
|
for (final Object item : videos) {
|
||||||
final JsonObject info = ((JsonObject) item)
|
final JsonObject info = ((JsonObject) item)
|
||||||
.getObject("musicResponsiveListItemRenderer", null);
|
.getObject("musicResponsiveListItemRenderer", null);
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
final String displayPolicy = info.getString("musicItemRendererDisplayPolicy", EMPTY_STRING);
|
final String displayPolicy = info.getString("musicItemRendererDisplayPolicy",
|
||||||
|
EMPTY_STRING);
|
||||||
if (displayPolicy.equals("MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")) {
|
if (displayPolicy.equals("MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")) {
|
||||||
continue; // no info about video URL available
|
continue; // No info about video URL available
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject flexColumnRenderer = info
|
final JsonObject flexColumnRenderer = info.getArray("flexColumns")
|
||||||
.getArray("flexColumns")
|
|
||||||
.getObject(1)
|
.getObject(1)
|
||||||
.getObject("musicResponsiveListItemFlexColumnRenderer");
|
.getObject("musicResponsiveListItemFlexColumnRenderer");
|
||||||
final JsonArray descriptionElements = flexColumnRenderer
|
final JsonArray descriptionElements = flexColumnRenderer.getObject("text")
|
||||||
.getObject("text")
|
|
||||||
.getArray("runs");
|
.getArray("runs");
|
||||||
final String searchType = getLinkHandler().getContentFilters().get(0);
|
final String searchType = getLinkHandler().getContentFilters().get(0);
|
||||||
if (searchType.equals(MUSIC_SONGS) || searchType.equals(MUSIC_VIDEOS)) {
|
if (searchType.equals(MUSIC_SONGS) || searchType.equals(MUSIC_VIDEOS)) {
|
||||||
collector.commit(new YoutubeStreamInfoItemExtractor(info, timeAgoParser) {
|
collector.commit(new YoutubeStreamInfoItemExtractor(info, timeAgoParser) {
|
||||||
@Override
|
@Override
|
||||||
public String getUrl() throws ParsingException {
|
public String getUrl() throws ParsingException {
|
||||||
final String id = info.getObject("playlistItemData").getString("videoId");
|
final String id = info.getObject("playlistItemData")
|
||||||
|
.getString("videoId");
|
||||||
if (!isNullOrEmpty(id)) {
|
if (!isNullOrEmpty(id)) {
|
||||||
return "https://music.youtube.com/watch?v=" + id;
|
return "https://music.youtube.com/watch?v=" + id;
|
||||||
}
|
}
|
||||||
@ -277,8 +298,10 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
final String name = getTextFromObject(info.getArray("flexColumns").getObject(0)
|
final String name = getTextFromObject(info.getArray("flexColumns")
|
||||||
.getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
|
.getObject(0)
|
||||||
|
.getObject("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
.getObject("text"));
|
||||||
if (!isNullOrEmpty(name)) {
|
if (!isNullOrEmpty(name)) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@ -308,23 +331,34 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getUploaderUrl() throws ParsingException {
|
public String getUploaderUrl() throws ParsingException {
|
||||||
if (searchType.equals(MUSIC_VIDEOS)) {
|
if (searchType.equals(MUSIC_VIDEOS)) {
|
||||||
JsonArray items = info.getObject("menu").getObject("menuRenderer").getArray("items");
|
JsonArray items = info.getObject("menu").getObject("menuRenderer")
|
||||||
for (Object item : items) {
|
.getArray("items");
|
||||||
final JsonObject menuNavigationItemRenderer = ((JsonObject) item).getObject("menuNavigationItemRenderer");
|
for (final Object item : items) {
|
||||||
if (menuNavigationItemRenderer.getObject("icon").getString("iconType", EMPTY_STRING).equals("ARTIST")) {
|
final JsonObject menuNavigationItemRenderer =
|
||||||
return getUrlFromNavigationEndpoint(menuNavigationItemRenderer.getObject("navigationEndpoint"));
|
((JsonObject) item).getObject(
|
||||||
|
"menuNavigationItemRenderer");
|
||||||
|
if (menuNavigationItemRenderer.getObject("icon")
|
||||||
|
.getString("iconType", EMPTY_STRING)
|
||||||
|
.equals("ARTIST")) {
|
||||||
|
return getUrlFromNavigationEndpoint(
|
||||||
|
menuNavigationItemRenderer
|
||||||
|
.getObject("navigationEndpoint"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
final JsonObject navigationEndpointHolder = info.getArray("flexColumns")
|
final JsonObject navigationEndpointHolder = info
|
||||||
.getObject(1).getObject("musicResponsiveListItemFlexColumnRenderer")
|
.getArray("flexColumns")
|
||||||
|
.getObject(1)
|
||||||
|
.getObject("musicResponsiveListItemFlexColumnRenderer")
|
||||||
.getObject("text").getArray("runs").getObject(0);
|
.getObject("text").getArray("runs").getObject(0);
|
||||||
|
|
||||||
if (!navigationEndpointHolder.has("navigationEndpoint")) return null;
|
if (!navigationEndpointHolder.has("navigationEndpoint"))
|
||||||
|
return null;
|
||||||
|
|
||||||
final String url = getUrlFromNavigationEndpoint(navigationEndpointHolder.getObject("navigationEndpoint"));
|
final String url = getUrlFromNavigationEndpoint(
|
||||||
|
navigationEndpointHolder.getObject("navigationEndpoint"));
|
||||||
|
|
||||||
if (!isNullOrEmpty(url)) {
|
if (!isNullOrEmpty(url)) {
|
||||||
return url;
|
return url;
|
||||||
@ -366,13 +400,15 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() throws ParsingException {
|
public String getThumbnailUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
final JsonArray thumbnails = info.getObject("thumbnail").getObject("musicThumbnailRenderer")
|
final JsonArray thumbnails = info.getObject("thumbnail")
|
||||||
|
.getObject("musicThumbnailRenderer")
|
||||||
.getObject("thumbnail").getArray("thumbnails");
|
.getObject("thumbnail").getArray("thumbnails");
|
||||||
// the last thumbnail is the one with the highest resolution
|
// the last thumbnail is the one with the highest resolution
|
||||||
final String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
|
final 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", e);
|
throw new ParsingException("Could not get thumbnail url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -382,21 +418,25 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() throws ParsingException {
|
public String getThumbnailUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
final JsonArray thumbnails = info.getObject("thumbnail").getObject("musicThumbnailRenderer")
|
final JsonArray thumbnails = info.getObject("thumbnail")
|
||||||
|
.getObject("musicThumbnailRenderer")
|
||||||
.getObject("thumbnail").getArray("thumbnails");
|
.getObject("thumbnail").getArray("thumbnails");
|
||||||
// the last thumbnail is the one with the highest resolution
|
// the last thumbnail is the one with the highest resolution
|
||||||
final String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
|
final 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", e);
|
throw new ParsingException("Could not get thumbnail url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
final String name = getTextFromObject(info.getArray("flexColumns").getObject(0)
|
final String name = getTextFromObject(info.getArray("flexColumns")
|
||||||
.getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
|
.getObject(0)
|
||||||
|
.getObject("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
.getObject("text"));
|
||||||
if (!isNullOrEmpty(name)) {
|
if (!isNullOrEmpty(name)) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@ -405,7 +445,8 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl() throws ParsingException {
|
public String getUrl() throws ParsingException {
|
||||||
final String url = getUrlFromNavigationEndpoint(info.getObject("navigationEndpoint"));
|
final String url = getUrlFromNavigationEndpoint(info
|
||||||
|
.getObject("navigationEndpoint"));
|
||||||
if (!isNullOrEmpty(url)) {
|
if (!isNullOrEmpty(url)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@ -414,8 +455,10 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSubscriberCount() throws ParsingException {
|
public long getSubscriberCount() throws ParsingException {
|
||||||
final String subscriberCount = getTextFromObject(info.getArray("flexColumns").getObject(2)
|
final String subscriberCount = getTextFromObject(info
|
||||||
.getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
|
.getArray("flexColumns").getObject(2)
|
||||||
|
.getObject("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
.getObject("text"));
|
||||||
if (!isNullOrEmpty(subscriberCount)) {
|
if (!isNullOrEmpty(subscriberCount)) {
|
||||||
try {
|
try {
|
||||||
return Utils.mixedNumberWordToLong(subscriberCount);
|
return Utils.mixedNumberWordToLong(subscriberCount);
|
||||||
@ -442,21 +485,25 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() throws ParsingException {
|
public String getThumbnailUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
final JsonArray thumbnails = info.getObject("thumbnail").getObject("musicThumbnailRenderer")
|
final JsonArray thumbnails = info.getObject("thumbnail")
|
||||||
|
.getObject("musicThumbnailRenderer")
|
||||||
.getObject("thumbnail").getArray("thumbnails");
|
.getObject("thumbnail").getArray("thumbnails");
|
||||||
// the last thumbnail is the one with the highest resolution
|
// the last thumbnail is the one with the highest resolution
|
||||||
final String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
|
final 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", e);
|
throw new ParsingException("Could not get thumbnail url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
final String name = getTextFromObject(info.getArray("flexColumns").getObject(0)
|
final String name = getTextFromObject(info.getArray("flexColumns")
|
||||||
.getObject("musicResponsiveListItemFlexColumnRenderer").getObject("text"));
|
.getObject(0)
|
||||||
|
.getObject("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
.getObject("text"));
|
||||||
if (!isNullOrEmpty(name)) {
|
if (!isNullOrEmpty(name)) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@ -509,7 +556,8 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
if (searchType.equals(MUSIC_ALBUMS)) {
|
if (searchType.equals(MUSIC_ALBUMS)) {
|
||||||
return ITEM_COUNT_UNKNOWN;
|
return ITEM_COUNT_UNKNOWN;
|
||||||
}
|
}
|
||||||
final String count = descriptionElements.getObject(2).getString("text");
|
final String count = descriptionElements.getObject(2)
|
||||||
|
.getString("text");
|
||||||
if (!isNullOrEmpty(count)) {
|
if (!isNullOrEmpty(count)) {
|
||||||
if (count.contains("100+")) {
|
if (count.contains("100+")) {
|
||||||
return ITEM_COUNT_MORE_THAN_100;
|
return ITEM_COUNT_MORE_THAN_100;
|
||||||
@ -525,17 +573,19 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPageFrom(final JsonArray continuations) throws ParsingException, IOException, ReCaptchaException {
|
@Nullable
|
||||||
|
private Page getNextPageFrom(final JsonArray continuations)
|
||||||
|
throws IOException, ParsingException, ReCaptchaException {
|
||||||
if (isNullOrEmpty(continuations)) {
|
if (isNullOrEmpty(continuations)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject nextContinuationData = continuations.getObject(0).getObject("nextContinuationData");
|
final JsonObject nextContinuationData = continuations.getObject(0)
|
||||||
|
.getObject("nextContinuationData");
|
||||||
final String continuation = nextContinuationData.getString("continuation");
|
final String continuation = nextContinuationData.getString("continuation");
|
||||||
final String clickTrackingParams = nextContinuationData.getString("clickTrackingParams");
|
|
||||||
|
|
||||||
return new Page("https://music.youtube.com/youtubei/v1/search?ctoken=" + continuation
|
return new Page("https://music.youtube.com/youtubei/v1/search?ctoken=" + continuation
|
||||||
+ "&continuation=" + continuation + "&itct=" + clickTrackingParams + "&alt=json"
|
+ "&continuation=" + continuation + "&alt=json" + "&key="
|
||||||
+ "&key=" + YoutubeParsingHelper.getYoutubeMusicKeys()[0]);
|
+ YoutubeParsingHelper.getYoutubeMusicKey()[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,36 +11,28 @@ import org.schabi.newpipe.extractor.downloader.Response;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
|
||||||
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.prepareJsonBuilder;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
||||||
private JsonArray initialAjaxJson;
|
|
||||||
private JsonObject initialData;
|
private JsonObject initialData;
|
||||||
private JsonObject playlistInfo;
|
private JsonObject playlistInfo;
|
||||||
|
|
||||||
@ -49,27 +41,35 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
||||||
final String url = getUrl() + "&pbj=1";
|
ExtractionException {
|
||||||
|
final Localization localization = getExtractorLocalization();
|
||||||
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(localization,
|
||||||
|
getExtractorContentCountry())
|
||||||
|
.value("browseId", "VL" + getId())
|
||||||
|
.value("params", "wgYCCAA%3D") // Show unavailable videos
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
initialAjaxJson = getJsonResponse(url, getExtractorLocalization());
|
initialData = getJsonPostResponse("browse", body, localization);
|
||||||
|
|
||||||
initialData = initialAjaxJson.getObject(1).getObject("response");
|
|
||||||
YoutubeParsingHelper.defaultAlertsCheck(initialData);
|
YoutubeParsingHelper.defaultAlertsCheck(initialData);
|
||||||
|
|
||||||
playlistInfo = getPlaylistInfo();
|
playlistInfo = getPlaylistInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonObject getUploaderInfo() throws ParsingException {
|
private JsonObject getUploaderInfo() throws ParsingException {
|
||||||
final JsonArray items = initialData.getObject("sidebar").getObject("playlistSidebarRenderer").getArray("items");
|
final JsonArray items = initialData.getObject("sidebar")
|
||||||
|
.getObject("playlistSidebarRenderer").getArray("items");
|
||||||
|
|
||||||
JsonObject videoOwner = items.getObject(1).getObject("playlistSidebarSecondaryInfoRenderer").getObject("videoOwner");
|
JsonObject videoOwner = items.getObject(1)
|
||||||
|
.getObject("playlistSidebarSecondaryInfoRenderer").getObject("videoOwner");
|
||||||
if (videoOwner.has("videoOwnerRenderer")) {
|
if (videoOwner.has("videoOwnerRenderer")) {
|
||||||
return videoOwner.getObject("videoOwnerRenderer");
|
return videoOwner.getObject("videoOwnerRenderer");
|
||||||
}
|
}
|
||||||
|
|
||||||
// we might want to create a loop here instead of using duplicated code
|
// we might want to create a loop here instead of using duplicated code
|
||||||
videoOwner = items.getObject(items.size()).getObject("playlistSidebarSecondaryInfoRenderer").getObject("videoOwner");
|
videoOwner = items.getObject(items.size())
|
||||||
|
.getObject("playlistSidebarSecondaryInfoRenderer").getObject("videoOwner");
|
||||||
if (videoOwner.has("videoOwnerRenderer")) {
|
if (videoOwner.has("videoOwnerRenderer")) {
|
||||||
return videoOwner.getObject("videoOwnerRenderer");
|
return videoOwner.getObject("videoOwnerRenderer");
|
||||||
}
|
}
|
||||||
@ -78,9 +78,10 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
|
|
||||||
private JsonObject getPlaylistInfo() throws ParsingException {
|
private JsonObject getPlaylistInfo() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return initialData.getObject("sidebar").getObject("playlistSidebarRenderer").getArray("items")
|
return initialData.getObject("sidebar").getObject("playlistSidebarRenderer")
|
||||||
.getObject(0).getObject("playlistSidebarPrimaryInfoRenderer");
|
.getArray("items").getObject(0)
|
||||||
} catch (Exception e) {
|
.getObject("playlistSidebarPrimaryInfoRenderer");
|
||||||
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get PlaylistInfo", e);
|
throw new ParsingException("Could not get PlaylistInfo", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +121,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
public String getUploaderUrl() throws ParsingException {
|
public String getUploaderUrl() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return getUrlFromNavigationEndpoint(getUploaderInfo().getObject("navigationEndpoint"));
|
return getUrlFromNavigationEndpoint(getUploaderInfo().getObject("navigationEndpoint"));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get playlist uploader url", e);
|
throw new ParsingException("Could not get playlist uploader url", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +130,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
public String getUploaderName() throws ParsingException {
|
public String getUploaderName() throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return getTextFromObject(getUploaderInfo().getObject("title"));
|
return getTextFromObject(getUploaderInfo().getObject("title"));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get playlist uploader name", e);
|
throw new ParsingException("Could not get playlist uploader name", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +141,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
final String url = getUploaderInfo().getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
|
final String url = getUploaderInfo().getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
|
||||||
|
|
||||||
return fixThumbnailUrl(url);
|
return fixThumbnailUrl(url);
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get playlist uploader avatar", e);
|
throw new ParsingException("Could not get playlist uploader avatar", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,7 +156,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
try {
|
try {
|
||||||
final String viewsText = getTextFromObject(getPlaylistInfo().getArray("stats").getObject(0));
|
final String viewsText = getTextFromObject(getPlaylistInfo().getArray("stats").getObject(0));
|
||||||
return Long.parseLong(Utils.removeNonDigitCharacters(viewsText));
|
return Long.parseLong(Utils.removeNonDigitCharacters(viewsText));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("Could not get video count from playlist", e);
|
throw new ParsingException("Could not get video count from playlist", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,18 +185,19 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
Page nextPage = null;
|
Page nextPage = null;
|
||||||
|
|
||||||
final JsonArray contents = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
|
final JsonArray contents = initialData.getObject("contents")
|
||||||
.getArray("tabs").getObject(0).getObject("tabRenderer").getObject("content")
|
.getObject("twoColumnBrowseResultsRenderer").getArray("tabs").getObject(0)
|
||||||
.getObject("sectionListRenderer").getArray("contents").getObject(0)
|
.getObject("tabRenderer").getObject("content").getObject("sectionListRenderer")
|
||||||
.getObject("itemSectionRenderer").getArray("contents");
|
.getArray("contents").getObject(0).getObject("itemSectionRenderer")
|
||||||
|
.getArray("contents");
|
||||||
|
|
||||||
if (contents.getObject(0).has("playlistSegmentRenderer")) {
|
if (contents.getObject(0).has("playlistSegmentRenderer")) {
|
||||||
for (final Object segment : contents) {
|
for (final Object segment : contents) {
|
||||||
if (((JsonObject) segment).getObject("playlistSegmentRenderer").has("trailer")) {
|
if (((JsonObject) segment).getObject("playlistSegmentRenderer")
|
||||||
collectTrailerFrom(collector, ((JsonObject) segment));
|
.has("videoList")) {
|
||||||
} else if (((JsonObject) segment).getObject("playlistSegmentRenderer").has("videoList")) {
|
collectStreamsFrom(collector, ((JsonObject) segment)
|
||||||
collectStreamsFrom(collector, ((JsonObject) segment).getObject("playlistSegmentRenderer")
|
.getObject("playlistSegmentRenderer").getObject("videoList")
|
||||||
.getObject("videoList").getObject("playlistVideoListRenderer").getArray("contents"));
|
.getObject("playlistVideoListRenderer").getArray("contents"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,20 +214,22 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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())) {
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
addClientInfoHeaders(headers);
|
||||||
|
|
||||||
final Response response = getDownloader().post(page.getUrl(), null, page.getBody(),
|
final Response response = getDownloader().post(page.getUrl(), headers, page.getBody(),
|
||||||
getExtractorLocalization());
|
getExtractorLocalization());
|
||||||
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
final JsonObject ajaxJson = JsonUtils.toJsonObject(getValidJsonResponseBody(response));
|
||||||
|
|
||||||
final JsonArray continuation = ajaxJson.getArray("onResponseReceivedActions")
|
final JsonArray continuation = ajaxJson.getArray("onResponseReceivedActions")
|
||||||
.getObject(0)
|
.getObject(0).getObject("appendContinuationItemsAction")
|
||||||
.getObject("appendContinuationItemsAction")
|
|
||||||
.getArray("continuationItems");
|
.getArray("continuationItems");
|
||||||
|
|
||||||
collectStreamsFrom(collector, continuation);
|
collectStreamsFrom(collector, continuation);
|
||||||
@ -233,7 +237,8 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
return new InfoItemsPage<>(collector, getNextPageFrom(continuation));
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuation));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPageFrom(final JsonArray contents) throws IOException, ExtractionException {
|
private Page getNextPageFrom(final JsonArray contents) throws IOException,
|
||||||
|
ExtractionException {
|
||||||
if (isNullOrEmpty(contents)) {
|
if (isNullOrEmpty(contents)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -246,25 +251,26 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
.getObject("continuationCommand")
|
.getObject("continuationCommand")
|
||||||
.getString("token");
|
.getString("token");
|
||||||
|
|
||||||
final byte[] body = JsonWriter.string(prepareJsonBuilder()
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
|
getExtractorLocalization(), getExtractorContentCountry())
|
||||||
.value("continuation", continuation)
|
.value("continuation", continuation)
|
||||||
.done())
|
.done())
|
||||||
.getBytes(UTF_8);
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
return new Page(
|
return new Page(YOUTUBEI_V1_URL + "browse?key=" + getKey(), body);
|
||||||
"https://www.youtube.com/youtubei/v1/browse?key=" + getKey(),
|
|
||||||
body);
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectStreamsFrom(final StreamInfoItemsCollector collector, final JsonArray videos) {
|
private void collectStreamsFrom(final StreamInfoItemsCollector collector,
|
||||||
|
final JsonArray videos) {
|
||||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
|
|
||||||
for (final Object video : videos) {
|
for (final Object video : videos) {
|
||||||
if (((JsonObject) video).has("playlistVideoRenderer")) {
|
if (((JsonObject) video).has("playlistVideoRenderer")) {
|
||||||
collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) video).getObject("playlistVideoRenderer"), timeAgoParser) {
|
collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) video)
|
||||||
|
.getObject("playlistVideoRenderer"), timeAgoParser) {
|
||||||
@Override
|
@Override
|
||||||
public long getViewCount() {
|
public long getViewCount() {
|
||||||
return -1;
|
return -1;
|
||||||
@ -273,81 +279,4 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectTrailerFrom(final StreamInfoItemsCollector collector,
|
|
||||||
final JsonObject segment) {
|
|
||||||
collector.commit(new StreamInfoItemExtractor() {
|
|
||||||
@Override
|
|
||||||
public String getName() throws ParsingException {
|
|
||||||
return getTextFromObject(segment.getObject("playlistSegmentRenderer")
|
|
||||||
.getObject("title"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUrl() throws ParsingException {
|
|
||||||
return YoutubeStreamLinkHandlerFactory.getInstance()
|
|
||||||
.fromId(segment.getObject("playlistSegmentRenderer").getObject("trailer")
|
|
||||||
.getObject("playlistVideoPlayerRenderer").getString("videoId"))
|
|
||||||
.getUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getThumbnailUrl() {
|
|
||||||
final JsonArray thumbnails = initialAjaxJson.getObject(1).getObject("playerResponse")
|
|
||||||
.getObject("videoDetails").getObject("thumbnail").getArray("thumbnails");
|
|
||||||
// the last thumbnail is the one with the highest resolution
|
|
||||||
final String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
|
|
||||||
return fixThumbnailUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StreamType getStreamType() {
|
|
||||||
return StreamType.VIDEO_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAd() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDuration() throws ParsingException {
|
|
||||||
return YoutubeParsingHelper.parseDurationString(
|
|
||||||
getTextFromObject(segment.getObject("playlistSegmentRenderer")
|
|
||||||
.getObject("segmentAnnotation")).split("•")[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getViewCount() {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUploaderName() throws ParsingException {
|
|
||||||
return YoutubePlaylistExtractor.this.getUploaderName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUploaderUrl() throws ParsingException {
|
|
||||||
return YoutubePlaylistExtractor.this.getUploaderUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUploaderVerified() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String getTextualUploadDate() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public DateWrapper getUploadDate() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector;
|
import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector;
|
||||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
@ -17,11 +18,10 @@ import org.schabi.newpipe.extractor.utils.JsonUtils;
|
|||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.getSearchParameter;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
@ -49,17 +49,37 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|||||||
public class YoutubeSearchExtractor extends SearchExtractor {
|
public class YoutubeSearchExtractor extends SearchExtractor {
|
||||||
private JsonObject initialData;
|
private JsonObject initialData;
|
||||||
|
|
||||||
public YoutubeSearchExtractor(final StreamingService service, final SearchQueryHandler linkHandler) {
|
public YoutubeSearchExtractor(final StreamingService service,
|
||||||
|
final SearchQueryHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException,
|
||||||
final String url = getUrl() + "&pbj=1";
|
ExtractionException {
|
||||||
|
final String query = super.getSearchString();
|
||||||
|
final Localization localization = getExtractorLocalization();
|
||||||
|
|
||||||
final JsonArray ajaxJson = getJsonResponse(url, getExtractorLocalization());
|
// Get the search parameter of the request
|
||||||
|
final List<String> contentFilters = super.getLinkHandler().getContentFilters();
|
||||||
|
final String params;
|
||||||
|
if (!isNullOrEmpty(contentFilters)) {
|
||||||
|
final String searchType = contentFilters.get(0);
|
||||||
|
params = getSearchParameter(searchType);
|
||||||
|
} else {
|
||||||
|
params = "";
|
||||||
|
}
|
||||||
|
|
||||||
initialData = ajaxJson.getObject(1).getObject("response");
|
final JsonBuilder<JsonObject> jsonBody = prepareDesktopJsonBuilder(localization,
|
||||||
|
getExtractorContentCountry())
|
||||||
|
.value("query", query);
|
||||||
|
if (!isNullOrEmpty(params)) {
|
||||||
|
jsonBody.value("params", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] body = JsonWriter.string(jsonBody.done()).getBytes(UTF_8);
|
||||||
|
|
||||||
|
initialData = getJsonPostResponse("search", body, localization);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@ -77,11 +97,13 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||||||
.getObject("itemSectionRenderer");
|
.getObject("itemSectionRenderer");
|
||||||
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents").getObject(0)
|
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents").getObject(0)
|
||||||
.getObject("didYouMeanRenderer");
|
.getObject("didYouMeanRenderer");
|
||||||
final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents").getObject(0)
|
final JsonObject showingResultsForRenderer = itemSectionRenderer.getArray("contents")
|
||||||
|
.getObject(0)
|
||||||
.getObject("showingResultsForRenderer");
|
.getObject("showingResultsForRenderer");
|
||||||
|
|
||||||
if (!didYouMeanRenderer.isEmpty()) {
|
if (!didYouMeanRenderer.isEmpty()) {
|
||||||
return JsonUtils.getString(didYouMeanRenderer, "correctedQueryEndpoint.searchEndpoint.query");
|
return JsonUtils.getString(didYouMeanRenderer,
|
||||||
|
"correctedQueryEndpoint.searchEndpoint.query");
|
||||||
} else if (showingResultsForRenderer != null) {
|
} else if (showingResultsForRenderer != null) {
|
||||||
return getTextFromObject(showingResultsForRenderer.getObject("correctedQuery"));
|
return getTextFromObject(showingResultsForRenderer.getObject("correctedQuery"));
|
||||||
} else {
|
} else {
|
||||||
@ -103,7 +125,8 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||||||
public List<MetaInfo> getMetaInfo() throws ParsingException {
|
public List<MetaInfo> getMetaInfo() throws ParsingException {
|
||||||
return YoutubeParsingHelper.getMetaInfo(
|
return YoutubeParsingHelper.getMetaInfo(
|
||||||
initialData.getObject("contents").getObject("twoColumnSearchResultsRenderer")
|
initialData.getObject("contents").getObject("twoColumnSearchResultsRenderer")
|
||||||
.getObject("primaryContents").getObject("sectionListRenderer").getArray("contents"));
|
.getObject("primaryContents").getObject("sectionListRenderer")
|
||||||
|
.getArray("contents"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@ -111,20 +134,21 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||||||
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||||
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
||||||
|
|
||||||
final JsonArray sections = initialData.getObject("contents").getObject("twoColumnSearchResultsRenderer")
|
final JsonArray sections = initialData.getObject("contents")
|
||||||
.getObject("primaryContents").getObject("sectionListRenderer").getArray("contents");
|
.getObject("twoColumnSearchResultsRenderer").getObject("primaryContents")
|
||||||
|
.getObject("sectionListRenderer").getArray("contents");
|
||||||
|
|
||||||
Page nextPage = null;
|
Page nextPage = null;
|
||||||
|
|
||||||
for (final Object section : sections) {
|
for (final Object section : sections) {
|
||||||
if (((JsonObject) section).has("itemSectionRenderer")) {
|
if (((JsonObject) section).has("itemSectionRenderer")) {
|
||||||
final JsonObject itemSectionRenderer = ((JsonObject) section).getObject("itemSectionRenderer");
|
final JsonObject itemSectionRenderer = ((JsonObject) section)
|
||||||
|
.getObject("itemSectionRenderer");
|
||||||
|
|
||||||
collectStreamsFrom(collector, itemSectionRenderer.getArray("contents"));
|
collectStreamsFrom(collector, itemSectionRenderer.getArray("contents"));
|
||||||
|
|
||||||
nextPage = getNextPageFrom(itemSectionRenderer.getArray("continuations"));
|
|
||||||
} else if (((JsonObject) section).has("continuationItemRenderer")) {
|
} else if (((JsonObject) section).has("continuationItemRenderer")) {
|
||||||
nextPage = getNewNextPageFrom(((JsonObject) section).getObject("continuationItemRenderer"));
|
nextPage = getNextPageFrom(((JsonObject) section)
|
||||||
|
.getObject("continuationItemRenderer"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,48 +156,25 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<InfoItem> getPage(final Page page) throws IOException, ExtractionException {
|
public InfoItemsPage<InfoItem> getPage(final Page page) throws IOException,
|
||||||
|
ExtractionException {
|
||||||
if (page == null || isNullOrEmpty(page.getUrl())) {
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||||
throw new IllegalArgumentException("Page doesn't contain an URL");
|
throw new IllegalArgumentException("Page doesn't contain an URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Localization localization = getExtractorLocalization();
|
||||||
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
final InfoItemsSearchCollector collector = new InfoItemsSearchCollector(getServiceId());
|
||||||
|
|
||||||
if (page.getId() == null) {
|
|
||||||
final JsonArray ajaxJson = getJsonResponse(page.getUrl(), getExtractorLocalization());
|
|
||||||
|
|
||||||
final JsonObject itemSectionContinuation = ajaxJson.getObject(1).getObject("response")
|
|
||||||
.getObject("continuationContents").getObject("itemSectionContinuation");
|
|
||||||
|
|
||||||
collectStreamsFrom(collector, itemSectionContinuation.getArray("contents"));
|
|
||||||
final JsonArray continuations = itemSectionContinuation.getArray("continuations");
|
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, getNextPageFrom(continuations));
|
|
||||||
} else {
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
final byte[] json = JsonWriter.string()
|
final byte[] json = JsonWriter.string(prepareDesktopJsonBuilder(localization,
|
||||||
.object()
|
getExtractorContentCountry())
|
||||||
.object("context")
|
|
||||||
.object("client")
|
|
||||||
.value("hl", "en")
|
|
||||||
.value("gl", getExtractorContentCountry().getCountryCode())
|
|
||||||
.value("clientName", "WEB")
|
|
||||||
.value("clientVersion", getClientVersion())
|
|
||||||
.value("utcOffsetMinutes", 0)
|
|
||||||
.end()
|
|
||||||
.object("request").end()
|
|
||||||
.object("user").end()
|
|
||||||
.end()
|
|
||||||
.value("continuation", page.getId())
|
.value("continuation", page.getId())
|
||||||
.end().done().getBytes(UTF_8);
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
final Map<String, List<String>> headers = new HashMap<>();
|
final String responseBody = getValidJsonResponseBody(getDownloader().post(
|
||||||
headers.put("Origin", Collections.singletonList("https://www.youtube.com"));
|
page.getUrl(), new HashMap<>(), json));
|
||||||
headers.put("Referer", Collections.singletonList(this.getUrl()));
|
|
||||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
|
||||||
|
|
||||||
final String responseBody = getValidJsonResponseBody(getDownloader().post(page.getUrl(), headers, json));
|
|
||||||
|
|
||||||
final JsonObject ajaxJson;
|
final JsonObject ajaxJson;
|
||||||
try {
|
try {
|
||||||
@ -183,47 +184,42 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final JsonArray continuationItems = ajaxJson.getArray("onResponseReceivedCommands")
|
final JsonArray continuationItems = ajaxJson.getArray("onResponseReceivedCommands")
|
||||||
.getObject(0).getObject("appendContinuationItemsAction").getArray("continuationItems");
|
.getObject(0).getObject("appendContinuationItemsAction")
|
||||||
|
.getArray("continuationItems");
|
||||||
|
|
||||||
final JsonArray contents = continuationItems.getObject(0).getObject("itemSectionRenderer").getArray("contents");
|
final JsonArray contents = continuationItems.getObject(0)
|
||||||
|
.getObject("itemSectionRenderer").getArray("contents");
|
||||||
collectStreamsFrom(collector, contents);
|
collectStreamsFrom(collector, contents);
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, getNewNextPageFrom(continuationItems.getObject(1).getObject("continuationItemRenderer")));
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuationItems.getObject(1)
|
||||||
}
|
.getObject("continuationItemRenderer")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectStreamsFrom(final InfoItemsSearchCollector collector, final JsonArray contents) throws NothingFoundException, ParsingException {
|
private void collectStreamsFrom(final InfoItemsSearchCollector collector,
|
||||||
|
final JsonArray contents) throws NothingFoundException,
|
||||||
|
ParsingException {
|
||||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
|
|
||||||
for (Object content : contents) {
|
for (final Object content : contents) {
|
||||||
final JsonObject item = (JsonObject) content;
|
final JsonObject item = (JsonObject) content;
|
||||||
if (item.has("backgroundPromoRenderer")) {
|
if (item.has("backgroundPromoRenderer")) {
|
||||||
throw new NothingFoundException(getTextFromObject(
|
throw new NothingFoundException(getTextFromObject(
|
||||||
item.getObject("backgroundPromoRenderer").getObject("bodyText")));
|
item.getObject("backgroundPromoRenderer").getObject("bodyText")));
|
||||||
} else if (item.has("videoRenderer")) {
|
} 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")) {
|
} else if (item.has("channelRenderer")) {
|
||||||
collector.commit(new YoutubeChannelInfoItemExtractor(item.getObject("channelRenderer")));
|
collector.commit(new YoutubeChannelInfoItemExtractor(item
|
||||||
|
.getObject("channelRenderer")));
|
||||||
} else if (item.has("playlistRenderer")) {
|
} else if (item.has("playlistRenderer")) {
|
||||||
collector.commit(new YoutubePlaylistInfoItemExtractor(item.getObject("playlistRenderer")));
|
collector.commit(new YoutubePlaylistInfoItemExtractor(item
|
||||||
|
.getObject("playlistRenderer")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPageFrom(final JsonArray continuations) throws ParsingException {
|
private Page getNextPageFrom(final JsonObject continuationItemRenderer) throws IOException,
|
||||||
if (isNullOrEmpty(continuations)) {
|
ExtractionException {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final JsonObject nextContinuationData = continuations.getObject(0).getObject("nextContinuationData");
|
|
||||||
final String continuation = nextContinuationData.getString("continuation");
|
|
||||||
final String clickTrackingParams = nextContinuationData.getString("clickTrackingParams");
|
|
||||||
|
|
||||||
return new Page(getUrl() + "&pbj=1&ctoken=" + continuation + "&continuation=" + continuation
|
|
||||||
+ "&itct=" + clickTrackingParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Page getNewNextPageFrom(final JsonObject continuationItemRenderer) throws IOException, ExtractionException {
|
|
||||||
if (isNullOrEmpty(continuationItemRenderer)) {
|
if (isNullOrEmpty(continuationItemRenderer)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -231,7 +227,7 @@ public class YoutubeSearchExtractor extends SearchExtractor {
|
|||||||
final String token = continuationItemRenderer.getObject("continuationEndpoint")
|
final String token = continuationItemRenderer.getObject("continuationEndpoint")
|
||||||
.getObject("continuationCommand").getString("token");
|
.getObject("continuationCommand").getString("token");
|
||||||
|
|
||||||
final String url = "https://www.youtube.com/youtubei/v1/search?key=" + getKey();
|
final String url = YOUTUBEI_V1_URL + "search?key=" + getKey();
|
||||||
|
|
||||||
return new Page(url, token);
|
return new Page(url, token);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
|
|||||||
|
|
||||||
import com.grack.nanojson.JsonArray;
|
import com.grack.nanojson.JsonArray;
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
@ -38,28 +39,32 @@ import java.io.IOException;
|
|||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextAtKey;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextAtKey;
|
||||||
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.UTF_8;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
public class YoutubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
|
public class YoutubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
|
||||||
private JsonObject initialData;
|
private JsonObject initialData;
|
||||||
|
|
||||||
public YoutubeTrendingExtractor(StreamingService service,
|
public YoutubeTrendingExtractor(final StreamingService service,
|
||||||
ListLinkHandler linkHandler,
|
final ListLinkHandler linkHandler,
|
||||||
String kioskId) {
|
final String kioskId) {
|
||||||
super(service, linkHandler, kioskId);
|
super(service, linkHandler, kioskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException {
|
||||||
final String url = getUrl() + "?pbj=1&gl="
|
// @formatter:off
|
||||||
+ getExtractorContentCountry().getCountryCode();
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(getExtractorLocalization(),
|
||||||
|
getExtractorContentCountry())
|
||||||
|
.value("browseId", "FEtrending")
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
final JsonArray ajaxJson = getJsonResponse(url, getExtractorLocalization());
|
initialData = getJsonPostResponse("browse", body, getExtractorLocalization());
|
||||||
|
|
||||||
initialData = ajaxJson.getObject(1).getObject("response");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -89,15 +94,17 @@ public class YoutubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
|
|||||||
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
||||||
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
JsonArray itemSectionRenderers = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
|
JsonArray itemSectionRenderers = initialData.getObject("contents")
|
||||||
.getArray("tabs").getObject(0).getObject("tabRenderer").getObject("content")
|
.getObject("twoColumnBrowseResultsRenderer").getArray("tabs").getObject(0)
|
||||||
.getObject("sectionListRenderer").getArray("contents");
|
.getObject("tabRenderer").getObject("content").getObject("sectionListRenderer")
|
||||||
|
.getArray("contents");
|
||||||
|
|
||||||
for (Object itemSectionRenderer : itemSectionRenderers) {
|
for (final Object itemSectionRenderer : itemSectionRenderers) {
|
||||||
JsonObject expandedShelfContentsRenderer = ((JsonObject) itemSectionRenderer).getObject("itemSectionRenderer")
|
JsonObject expandedShelfContentsRenderer = ((JsonObject) itemSectionRenderer)
|
||||||
.getArray("contents").getObject(0).getObject("shelfRenderer").getObject("content")
|
.getObject("itemSectionRenderer").getArray("contents").getObject(0)
|
||||||
|
.getObject("shelfRenderer").getObject("content")
|
||||||
.getObject("expandedShelfContentsRenderer");
|
.getObject("expandedShelfContentsRenderer");
|
||||||
for (Object ul : expandedShelfContentsRenderer.getArray("items")) {
|
for (final Object ul : expandedShelfContentsRenderer.getArray("items")) {
|
||||||
final JsonObject videoInfo = ((JsonObject) ul).getObject("videoRenderer");
|
final JsonObject videoInfo = ((JsonObject) ul).getObject("videoRenderer");
|
||||||
collector.commit(new YoutubeStreamInfoItemExtractor(videoInfo, timeAgoParser));
|
collector.commit(new YoutubeStreamInfoItemExtractor(videoInfo, timeAgoParser));
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ public class YoutubeCommentsLinkHandlerFactory extends ListLinkHandlerFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl(String id) {
|
public String getUrl(String id) {
|
||||||
return "https://m.youtube.com/watch?v=" + id;
|
return "https://www.youtube.com/watch?v=" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -3,11 +3,13 @@ package org.schabi.newpipe.extractor.services.youtube.linkHandler;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
|
public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
|
||||||
|
|
||||||
@ -25,24 +27,31 @@ public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory
|
|||||||
private static final String SEARCH_URL = "https://www.youtube.com/results?search_query=";
|
private static final String SEARCH_URL = "https://www.youtube.com/results?search_query=";
|
||||||
private static final String MUSIC_SEARCH_URL = "https://music.youtube.com/search?q=";
|
private static final String MUSIC_SEARCH_URL = "https://music.youtube.com/search?q=";
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
public static YoutubeSearchQueryHandlerFactory getInstance() {
|
public static YoutubeSearchQueryHandlerFactory getInstance() {
|
||||||
return new YoutubeSearchQueryHandlerFactory();
|
return new YoutubeSearchQueryHandlerFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl(String searchString, List<String> contentFilters, String sortFilter) throws ParsingException {
|
public String getUrl(final String searchString,
|
||||||
|
@Nonnull final List<String> contentFilters,
|
||||||
|
final String sortFilter) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
if (!contentFilters.isEmpty()) {
|
if (!contentFilters.isEmpty()) {
|
||||||
switch (contentFilters.get(0)) {
|
final String contentFilter = contentFilters.get(0);
|
||||||
|
switch (contentFilter) {
|
||||||
case ALL:
|
case ALL:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
case VIDEOS:
|
case VIDEOS:
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8) + "&sp=EgIQAQ%253D%253D";
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8)
|
||||||
|
+ "&sp=EgIQAQ%253D%253D";
|
||||||
case CHANNELS:
|
case CHANNELS:
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8) + "&sp=EgIQAg%253D%253D";
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8)
|
||||||
|
+ "&sp=EgIQAg%253D%253D";
|
||||||
case PLAYLISTS:
|
case PLAYLISTS:
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8) + "&sp=EgIQAw%253D%253D";
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8)
|
||||||
|
+ "&sp=EgIQAw%253D%253D";
|
||||||
case MUSIC_SONGS:
|
case MUSIC_SONGS:
|
||||||
case MUSIC_VIDEOS:
|
case MUSIC_VIDEOS:
|
||||||
case MUSIC_ALBUMS:
|
case MUSIC_ALBUMS:
|
||||||
@ -53,7 +62,7 @@ public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8);
|
return SEARCH_URL + URLEncoder.encode(searchString, UTF_8);
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (final UnsupportedEncodingException e) {
|
||||||
throw new ParsingException("Could not encode query", e);
|
throw new ParsingException("Could not encode query", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,7 +78,28 @@ public class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory
|
|||||||
MUSIC_VIDEOS,
|
MUSIC_VIDEOS,
|
||||||
MUSIC_ALBUMS,
|
MUSIC_ALBUMS,
|
||||||
MUSIC_PLAYLISTS
|
MUSIC_PLAYLISTS
|
||||||
// MUSIC_ARTISTS
|
// MUSIC_ARTISTS
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public static String getSearchParameter(final String contentFilter) {
|
||||||
|
if (isNullOrEmpty(contentFilter)) return "";
|
||||||
|
switch (contentFilter) {
|
||||||
|
case VIDEOS:
|
||||||
|
return "EgIQAQ%3D%3D";
|
||||||
|
case CHANNELS:
|
||||||
|
return "EgIQAg%3D%3D";
|
||||||
|
case PLAYLISTS:
|
||||||
|
return "EgIQAw%3D%3D";
|
||||||
|
case ALL:
|
||||||
|
case MUSIC_SONGS:
|
||||||
|
case MUSIC_VIDEOS:
|
||||||
|
case MUSIC_ALBUMS:
|
||||||
|
case MUSIC_PLAYLISTS:
|
||||||
|
case MUSIC_ARTISTS:
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
package org.schabi.newpipe.extractor.stream;
|
package org.schabi.newpipe.extractor.stream;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class Frameset {
|
public final class Frameset implements Serializable {
|
||||||
|
|
||||||
private List<String> urls;
|
private final List<String> urls;
|
||||||
private int frameWidth;
|
private final int frameWidth;
|
||||||
private int frameHeight;
|
private final int frameHeight;
|
||||||
private int totalCount;
|
private final int totalCount;
|
||||||
private int durationPerFrame;
|
private final int durationPerFrame;
|
||||||
private int framesPerPageX;
|
private final int framesPerPageX;
|
||||||
private int framesPerPageY;
|
private final int framesPerPageY;
|
||||||
|
|
||||||
|
public Frameset(
|
||||||
|
final List<String> urls,
|
||||||
|
final int frameWidth,
|
||||||
|
final int frameHeight,
|
||||||
|
final int totalCount,
|
||||||
|
final int durationPerFrame,
|
||||||
|
final int framesPerPageX,
|
||||||
|
final int framesPerPageY) {
|
||||||
|
|
||||||
public Frameset(List<String> urls, int frameWidth, int frameHeight, int totalCount, int durationPerFrame, int framesPerPageX, int framesPerPageY) {
|
|
||||||
this.urls = urls;
|
this.urls = urls;
|
||||||
this.totalCount = totalCount;
|
this.totalCount = totalCount;
|
||||||
this.durationPerFrame = durationPerFrame;
|
this.durationPerFrame = durationPerFrame;
|
||||||
@ -86,7 +95,7 @@ public final class Frameset {
|
|||||||
* <li><code>4</code>: Bottom bound</li>
|
* <li><code>4</code>: Bottom bound</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public int[] getFrameBoundsAt(long position) {
|
public int[] getFrameBoundsAt(final long position) {
|
||||||
if (position < 0 || position > ((totalCount + 1) * durationPerFrame)) {
|
if (position < 0 || position > ((totalCount + 1) * durationPerFrame)) {
|
||||||
// Return the first frame as fallback
|
// Return the first frame as fallback
|
||||||
return new int[] { 0, 0, 0, frameWidth, frameHeight };
|
return new int[] { 0, 0, 0, frameWidth, frameHeight };
|
||||||
|
@ -335,6 +335,12 @@ public class StreamInfo extends Info {
|
|||||||
streamInfo.addError(e);
|
streamInfo.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
streamInfo.setPreviewFrames(extractor.getFrames());
|
||||||
|
} catch (Exception e) {
|
||||||
|
streamInfo.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
streamInfo.setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo, extractor));
|
streamInfo.setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo, extractor));
|
||||||
|
|
||||||
return streamInfo;
|
return streamInfo;
|
||||||
@ -386,6 +392,11 @@ public class StreamInfo extends Info {
|
|||||||
private List<StreamSegment> streamSegments = new ArrayList<>();
|
private List<StreamSegment> streamSegments = new ArrayList<>();
|
||||||
private List<MetaInfo> metaInfo = new ArrayList<>();
|
private List<MetaInfo> metaInfo = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview frames, e.g. for the storyboard / seekbar thumbnail preview
|
||||||
|
*/
|
||||||
|
private List<Frameset> previewFrames = Collections.emptyList();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the stream type
|
* Get the stream type
|
||||||
*
|
*
|
||||||
@ -711,6 +722,14 @@ public class StreamInfo extends Info {
|
|||||||
this.metaInfo = metaInfo;
|
this.metaInfo = metaInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Frameset> getPreviewFrames() {
|
||||||
|
return previewFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPreviewFrames(final List<Frameset> previewFrames) {
|
||||||
|
this.previewFrames = previewFrames;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public List<MetaInfo> getMetaInfo() {
|
public List<MetaInfo> getMetaInfo() {
|
||||||
return this.metaInfo;
|
return this.metaInfo;
|
||||||
|
@ -79,8 +79,13 @@ public class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isMatch(String pattern, String input) {
|
public static boolean isMatch(String pattern, String input) {
|
||||||
Pattern pat = Pattern.compile(pattern);
|
final Pattern pat = Pattern.compile(pattern);
|
||||||
Matcher mat = pat.matcher(input);
|
final Matcher mat = pat.matcher(input);
|
||||||
|
return mat.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isMatch(Pattern pattern, String input) {
|
||||||
|
final Matcher mat = pattern.matcher(input);
|
||||||
return mat.find();
|
return mat.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package org.schabi.newpipe.extractor.utils;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class StringUtils {
|
||||||
|
|
||||||
|
private StringUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string The string to search in.
|
||||||
|
* @param start A string from which to start searching.
|
||||||
|
* @return A substring where each '{' matches a '}'.
|
||||||
|
* @throws IndexOutOfBoundsException If {@code string} does not contain {@code start}
|
||||||
|
* or parenthesis could not be matched .
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static String matchToClosingParenthesis(@Nonnull final String string, @Nonnull final String start) {
|
||||||
|
int startIndex = string.indexOf(start);
|
||||||
|
if (startIndex < 0) {
|
||||||
|
throw new IndexOutOfBoundsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex += start.length();
|
||||||
|
int endIndex = startIndex;
|
||||||
|
while (string.charAt(endIndex) != '{') {
|
||||||
|
++endIndex;
|
||||||
|
}
|
||||||
|
++endIndex;
|
||||||
|
|
||||||
|
int openParenthesis = 1;
|
||||||
|
while (openParenthesis > 0) {
|
||||||
|
switch (string.charAt(endIndex)) {
|
||||||
|
case '{':
|
||||||
|
++openParenthesis;
|
||||||
|
break;
|
||||||
|
case '}':
|
||||||
|
--openParenthesis;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++endIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.substring(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
}
|
@ -59,7 +59,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCTGjY2I-ZUGnwVoWAGRd7XQ");
|
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCTGjY2I-ZUGnwVoWAGRd7XQ");
|
||||||
try {
|
try {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
} catch (AccountTerminatedException e) {
|
} catch (final AccountTerminatedException e) {
|
||||||
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
YouTube.getChannelExtractor("https://www.youtube.com/channel/UC0AuOxCr9TZ0TtEgL1zpIgA");
|
YouTube.getChannelExtractor("https://www.youtube.com/channel/UC0AuOxCr9TZ0TtEgL1zpIgA");
|
||||||
try {
|
try {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
} catch (AccountTerminatedException e) {
|
} catch (final AccountTerminatedException e) {
|
||||||
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCPWXIOPK-9myzek6jHR5yrg");
|
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCPWXIOPK-9myzek6jHR5yrg");
|
||||||
try {
|
try {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
} catch (AccountTerminatedException e) {
|
} catch (final AccountTerminatedException e) {
|
||||||
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
YouTube.getChannelExtractor("https://youtube.com/channel/UCB1o7_gbFp2PLsamWxFenBg");
|
YouTube.getChannelExtractor("https://youtube.com/channel/UCB1o7_gbFp2PLsamWxFenBg");
|
||||||
try {
|
try {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
} catch (AccountTerminatedException e) {
|
} catch (final AccountTerminatedException e) {
|
||||||
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -115,7 +115,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCoaO4U_p7G7AwalqSbGCZOA");
|
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCoaO4U_p7G7AwalqSbGCZOA");
|
||||||
try {
|
try {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
} catch (AccountTerminatedException e) {
|
} catch (final AccountTerminatedException e) {
|
||||||
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCpExuV8qJMfCaSQNL1YG6bQ");
|
YouTube.getChannelExtractor("https://www.youtube.com/channel/UCpExuV8qJMfCaSQNL1YG6bQ");
|
||||||
try {
|
try {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
} catch (AccountTerminatedException e) {
|
} catch (final AccountTerminatedException e) {
|
||||||
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
assertEquals(e.getReason(), AccountTerminatedException.Reason.VIOLATION);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -619,7 +619,7 @@ public class YoutubeChannelExtractorTest {
|
|||||||
public void testMoreRelatedItems() {
|
public void testMoreRelatedItems() {
|
||||||
try {
|
try {
|
||||||
defaultTestMoreItems(extractor);
|
defaultTestMoreItems(extractor);
|
||||||
} catch (Throwable ignored) {
|
} catch (final Throwable ignored) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -667,4 +667,3 @@ public class YoutubeChannelExtractorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +40,10 @@ public class YoutubeChannelLocalizationTest {
|
|||||||
testLocalizationsFor("https://www.youtube.com/channel/UCEOXxzW2vU0P-0THehuIIeg");
|
testLocalizationsFor("https://www.youtube.com/channel/UCEOXxzW2vU0P-0THehuIIeg");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testLocalizationsFor(String channelUrl) throws Exception {
|
private void testLocalizationsFor(final String channelUrl) throws Exception {
|
||||||
|
|
||||||
final List<Localization> supportedLocalizations = YouTube.getSupportedLocalizations();
|
final List<Localization> supportedLocalizations = YouTube.getSupportedLocalizations();
|
||||||
// final List<Localization> supportedLocalizations = Arrays.asList(Localization.DEFAULT, new Localization("sr"));
|
// final List<Localization> supportedLocalizations = Arrays.asList(Localization.DEFAULT, new Localization("sr"));
|
||||||
final Map<Localization, List<StreamInfoItem>> results = new LinkedHashMap<>();
|
final Map<Localization, List<StreamInfoItem>> results = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (Localization currentLocalization : supportedLocalizations) {
|
for (Localization currentLocalization : supportedLocalizations) {
|
||||||
@ -55,7 +55,7 @@ public class YoutubeChannelLocalizationTest {
|
|||||||
extractor.forceLocalization(currentLocalization);
|
extractor.forceLocalization(currentLocalization);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
itemsPage = defaultTestRelatedItems(extractor);
|
itemsPage = defaultTestRelatedItems(extractor);
|
||||||
} catch (Throwable e) {
|
} catch (final Throwable e) {
|
||||||
System.out.println("[!] " + currentLocalization + " → failed");
|
System.out.println("[!] " + currentLocalization + " → failed");
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class YouTubeCommentsLinkHandlerFactoryTest {
|
public class YoutubeCommentsLinkHandlerFactoryTest {
|
||||||
|
|
||||||
private static YoutubeCommentsLinkHandlerFactory linkHandler;
|
private static YoutubeCommentsLinkHandlerFactory linkHandler;
|
||||||
|
|
@ -93,6 +93,5 @@ public class YoutubeFeedExtractorTest {
|
|||||||
.getFeedExtractor("https://www.youtube.com/channel/UCTGjY2I-ZUGnwVoWAGRd7XQ");
|
.getFeedExtractor("https://www.youtube.com/channel/UCTGjY2I-ZUGnwVoWAGRd7XQ");
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,23 +1,16 @@
|
|||||||
package org.schabi.newpipe.extractor.services.youtube;
|
package org.schabi.newpipe.extractor.services.youtube;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
import org.hamcrest.MatcherAssert;
|
import org.hamcrest.MatcherAssert;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.Suite;
|
|
||||||
import org.junit.runners.Suite.SuiteClasses;
|
|
||||||
import org.schabi.newpipe.downloader.DownloaderFactory;
|
import org.schabi.newpipe.downloader.DownloaderFactory;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeMixPlaylistExtractorTest.ChannelMix;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeMixPlaylistExtractorTest.Invalid;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeMixPlaylistExtractorTest.Mix;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeMixPlaylistExtractorTest.MixWithIndex;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeMixPlaylistExtractorTest.MyMix;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMixPlaylistExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMixPlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
@ -32,12 +25,11 @@ import static org.junit.Assert.assertFalse;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl;
|
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl;
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
|
|
||||||
@RunWith(Suite.class)
|
|
||||||
@SuiteClasses({Mix.class, MixWithIndex.class, MyMix.class, Invalid.class, ChannelMix.class})
|
|
||||||
public class YoutubeMixPlaylistExtractorTest {
|
public class YoutubeMixPlaylistExtractorTest {
|
||||||
|
|
||||||
public static final String PBJ = "&pbj=1";
|
|
||||||
private static final String VIDEO_ID = "_AzeUSL9lZc";
|
private static final String VIDEO_ID = "_AzeUSL9lZc";
|
||||||
private static final String VIDEO_TITLE =
|
private static final String VIDEO_TITLE =
|
||||||
"Most Beautiful And Emotional Piano: Anime Music Shigatsu wa Kimi no Uso OST IMO";
|
"Most Beautiful And Emotional Piano: Anime Music Shigatsu wa Kimi no Uso OST IMO";
|
||||||
@ -46,6 +38,7 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
|
|
||||||
private static YoutubeMixPlaylistExtractor extractor;
|
private static YoutubeMixPlaylistExtractor extractor;
|
||||||
|
|
||||||
|
@Ignore("Test broken, video was blocked by SME and is only available in Japan")
|
||||||
public static class Mix {
|
public static class Mix {
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@ -55,8 +48,8 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "mix"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "mix"));
|
||||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
.getPlaylistExtractor(
|
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||||
"https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RD" + VIDEO_ID);
|
+ "&list=RD" + VIDEO_ID);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +82,16 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getPage() throws Exception {
|
public void getPage() throws Exception {
|
||||||
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
new Page("https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RD" + VIDEO_ID
|
NewPipe.getPreferredLocalization(), NewPipe.getPreferredContentCountry())
|
||||||
+ PBJ, dummyCookie));
|
.value("videoId", VIDEO_ID)
|
||||||
|
.value("playlistId", "RD" + VIDEO_ID)
|
||||||
|
.value("params", "OAE%3D")
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(new Page(
|
||||||
|
YOUTUBEI_V1_URL + "next?key=" + getKey(), null, null, dummyCookie, body));
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
}
|
}
|
||||||
@ -101,14 +101,14 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
final Set<String> urls = new HashSet<>();
|
final Set<String> urls = new HashSet<>();
|
||||||
|
|
||||||
//Should work infinitely, but for testing purposes only 3 times
|
// Should work infinitely, but for testing purposes only 3 times
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
|
||||||
for (final StreamInfoItem item : streams.getItems()) {
|
for (final StreamInfoItem item : streams.getItems()) {
|
||||||
// TODO Duplicates are appearing
|
// TODO Duplicates are appearing
|
||||||
// assertFalse(urls.contains(item.getUrl()));
|
// assertFalse(urls.contains(item.getUrl()));
|
||||||
urls.add(item.getUrl());
|
urls.add(item.getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,10 +124,10 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore("Test broken, video was removed by the uploader")
|
||||||
public static class MixWithIndex {
|
public static class MixWithIndex {
|
||||||
|
|
||||||
private static final String INDEX = "&index=13";
|
private static final int INDEX = 13;
|
||||||
private static final String VIDEO_ID_NUMBER_13 = "qHtzO49SDmk";
|
private static final String VIDEO_ID_NUMBER_13 = "qHtzO49SDmk";
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@ -137,9 +137,8 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "mixWithIndex"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "mixWithIndex"));
|
||||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
.getPlaylistExtractor(
|
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID_NUMBER_13
|
||||||
"https://www.youtube.com/watch?v=" + VIDEO_ID_NUMBER_13 + "&list=RD"
|
+ "&list=RD" + VIDEO_ID + "&index=" + INDEX);
|
||||||
+ VIDEO_ID + INDEX);
|
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,9 +166,17 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getPage() throws Exception {
|
public void getPage() throws Exception {
|
||||||
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
new Page("https://www.youtube.com/watch?v=" + VIDEO_ID_NUMBER_13 + "&list=RD"
|
NewPipe.getPreferredLocalization(), NewPipe.getPreferredContentCountry())
|
||||||
+ VIDEO_ID + INDEX + PBJ, dummyCookie));
|
.value("videoId", VIDEO_ID)
|
||||||
|
.value("playlistId", "RD" + VIDEO_ID)
|
||||||
|
.value("playlistIndex", INDEX)
|
||||||
|
.value("params", "OAE%3D")
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(new Page(
|
||||||
|
YOUTUBEI_V1_URL + "next?key=" + getKey(), null, null, dummyCookie, body));
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
}
|
}
|
||||||
@ -179,13 +186,13 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
final Set<String> urls = new HashSet<>();
|
final Set<String> urls = new HashSet<>();
|
||||||
|
|
||||||
//Should work infinitely, but for testing purposes only 3 times
|
// Should work infinitely, but for testing purposes only 3 times
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
for (final StreamInfoItem item : streams.getItems()) {
|
for (final StreamInfoItem item : streams.getItems()) {
|
||||||
// TODO Duplicates are appearing
|
// TODO Duplicates are appearing
|
||||||
// assertFalse(urls.contains(item.getUrl()));
|
// assertFalse(urls.contains(item.getUrl()));
|
||||||
urls.add(item.getUrl());
|
urls.add(item.getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,6 +208,7 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Test broken")
|
||||||
public static class MyMix {
|
public static class MyMix {
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@ -210,9 +218,8 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "myMix"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "myMix"));
|
||||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
.getPlaylistExtractor(
|
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||||
"https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RDMM"
|
+ "&list=RDMM" + VIDEO_ID);
|
||||||
+ VIDEO_ID);
|
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,9 +250,16 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getPage() throws Exception {
|
public void getPage() throws Exception {
|
||||||
final InfoItemsPage<StreamInfoItem> streams =
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
extractor.getPage(new Page("https://www.youtube.com/watch?v=" + VIDEO_ID
|
NewPipe.getPreferredLocalization(), NewPipe.getPreferredContentCountry())
|
||||||
+ "&list=RDMM" + VIDEO_ID + PBJ, dummyCookie));
|
.value("videoId", VIDEO_ID)
|
||||||
|
.value("playlistId", "RDMM" + VIDEO_ID)
|
||||||
|
.value("params", "OAE%3D")
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(new Page(
|
||||||
|
YOUTUBEI_V1_URL + "next?key=" + getKey(), null, null, dummyCookie, body));
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
}
|
}
|
||||||
@ -255,14 +269,14 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
final Set<String> urls = new HashSet<>();
|
final Set<String> urls = new HashSet<>();
|
||||||
|
|
||||||
//Should work infinitely, but for testing purposes only 3 times
|
// Should work infinitely, but for testing purposes only 3 times
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
|
||||||
for (final StreamInfoItem item : streams.getItems()) {
|
for (final StreamInfoItem item : streams.getItems()) {
|
||||||
// TODO Duplicates are appearing
|
// TODO Duplicates are appearing
|
||||||
// assertFalse(urls.contains(item.getUrl()));
|
// assertFalse(urls.contains(item.getUrl()));
|
||||||
urls.add(item.getUrl());
|
urls.add(item.getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,11 +302,12 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void getPageEmptyUrl() throws Exception {
|
public void getPageEmptyUrl() throws Exception {
|
||||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
.getPlaylistExtractor(
|
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||||
"https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RD" + VIDEO_ID);
|
+ "&list=RD" + VIDEO_ID);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
extractor.getPage(new Page(""));
|
extractor.getPage(new Page(""));
|
||||||
}
|
}
|
||||||
@ -300,8 +315,8 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
@Test(expected = ExtractionException.class)
|
@Test(expected = ExtractionException.class)
|
||||||
public void invalidVideoId() throws Exception {
|
public void invalidVideoId() throws Exception {
|
||||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
.getPlaylistExtractor(
|
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + "abcde"
|
||||||
"https://www.youtube.com/watch?v=" + "abcde" + "&list=RD" + "abcde");
|
+ "&list=RD" + "abcde");
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
extractor.getName();
|
extractor.getName();
|
||||||
}
|
}
|
||||||
@ -321,8 +336,7 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "channelMix"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "channelMix"));
|
||||||
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
dummyCookie.put(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||||
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
.getPlaylistExtractor(
|
.getPlaylistExtractor("https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
|
||||||
"https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
|
|
||||||
+ "&list=RDCM" + CHANNEL_ID);
|
+ "&list=RDCM" + CHANNEL_ID);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
@ -350,9 +364,16 @@ public class YoutubeMixPlaylistExtractorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getPage() throws Exception {
|
public void getPage() throws Exception {
|
||||||
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(
|
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(
|
||||||
new Page("https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
|
NewPipe.getPreferredLocalization(), NewPipe.getPreferredContentCountry())
|
||||||
+ "&list=RDCM" + CHANNEL_ID + PBJ, dummyCookie));
|
.value("videoId", VIDEO_ID_OF_CHANNEL)
|
||||||
|
.value("playlistId", "RDCM" + CHANNEL_ID)
|
||||||
|
.value("params", "OAE%3D")
|
||||||
|
.done())
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(new Page(
|
||||||
|
YOUTUBEI_V1_URL + "next?key=" + getKey(), null, null, dummyCookie, body));
|
||||||
assertFalse(streams.getItems().isEmpty());
|
assertFalse(streams.getItems().isEmpty());
|
||||||
assertTrue(streams.hasNextPage());
|
assertTrue(streams.hasNextPage());
|
||||||
}
|
}
|
||||||
|
@ -25,15 +25,15 @@ public class YoutubeParsingHelperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsHardcodedClientVersionValid() throws IOException, ExtractionException {
|
public void testAreHardcodedClientVersionAndKeyValid() throws IOException, ExtractionException {
|
||||||
assertTrue("Hardcoded client version is not valid anymore",
|
assertTrue("Hardcoded client version and key are not valid anymore",
|
||||||
YoutubeParsingHelper.isHardcodedClientVersionValid());
|
YoutubeParsingHelper.areHardcodedClientVersionAndKeyValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAreHardcodedYoutubeMusicKeysValid() throws IOException, ExtractionException {
|
public void testAreHardcodedYoutubeMusicKeysValid() throws IOException, ExtractionException {
|
||||||
assertTrue("Hardcoded YouTube Music keys are not valid anymore",
|
assertTrue("Hardcoded YouTube Music keys are not valid anymore",
|
||||||
YoutubeParsingHelper.areHardcodedYoutubeMusicKeysValid());
|
YoutubeParsingHelper.isHardcodedYoutubeMusicKeyValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -44,7 +44,7 @@ public class YoutubeParsingHelperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConvertFromGoogleCacheUrl() throws ParsingException {
|
public void testConvertFromGoogleCacheUrl() {
|
||||||
assertEquals("https://mohfw.gov.in/",
|
assertEquals("https://mohfw.gov.in/",
|
||||||
YoutubeParsingHelper.extractCachedUrlIfNeeded("https://webcache.googleusercontent.com/search?q=cache:https://mohfw.gov.in/"));
|
YoutubeParsingHelper.extractCachedUrlIfNeeded("https://webcache.googleusercontent.com/search?q=cache:https://mohfw.gov.in/"));
|
||||||
assertEquals("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html",
|
assertEquals("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html",
|
||||||
|
@ -3,9 +3,6 @@ package org.schabi.newpipe.extractor.services.youtube;
|
|||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.Suite;
|
|
||||||
import org.junit.runners.Suite.SuiteClasses;
|
|
||||||
import org.schabi.newpipe.downloader.DownloaderFactory;
|
import org.schabi.newpipe.downloader.DownloaderFactory;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
@ -13,11 +10,6 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.BasePlaylistExtractorTest;
|
import org.schabi.newpipe.extractor.services.BasePlaylistExtractorTest;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubePlaylistExtractorTest.ContinuationsTests;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubePlaylistExtractorTest.HugePlaylist;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubePlaylistExtractorTest.LearningPlaylist;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubePlaylistExtractorTest.NotAvailable;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubePlaylistExtractorTest.TimelessPopHits;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubePlaylistExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubePlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
@ -38,9 +30,6 @@ import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestRela
|
|||||||
/**
|
/**
|
||||||
* Test for {@link YoutubePlaylistExtractor}
|
* Test for {@link YoutubePlaylistExtractor}
|
||||||
*/
|
*/
|
||||||
@RunWith(Suite.class)
|
|
||||||
@SuiteClasses({NotAvailable.class, TimelessPopHits.class, HugePlaylist.class,
|
|
||||||
LearningPlaylist.class, ContinuationsTests.class})
|
|
||||||
public class YoutubePlaylistExtractorTest {
|
public class YoutubePlaylistExtractorTest {
|
||||||
|
|
||||||
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/playlist/";
|
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/playlist/";
|
||||||
@ -61,7 +50,6 @@ public class YoutubePlaylistExtractorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = ContentNotAvailableException.class)
|
@Test(expected = ContentNotAvailableException.class)
|
||||||
@Ignore("Broken, now invalid playlists redirect to youtube homepage")
|
|
||||||
public void invalidId() throws Exception {
|
public void invalidId() throws Exception {
|
||||||
final PlaylistExtractor extractor =
|
final PlaylistExtractor extractor =
|
||||||
YouTube.getPlaylistExtractor("https://www.youtube.com/playlist?list=INVALID_ID");
|
YouTube.getPlaylistExtractor("https://www.youtube.com/playlist?list=INVALID_ID");
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.extractor.services.youtube;
|
|||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mozilla.javascript.EvaluatorException;
|
||||||
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.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
@ -11,6 +12,7 @@ import java.io.IOException;
|
|||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class YoutubeThrottlingDecrypterTest {
|
public class YoutubeThrottlingDecrypterTest {
|
||||||
|
|
||||||
@ -19,6 +21,22 @@ public class YoutubeThrottlingDecrypterTest {
|
|||||||
NewPipe.init(DownloaderTestImpl.getInstance());
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractFunction__success() throws ParsingException {
|
||||||
|
final String[] videoIds = {"jE1USQrs1rw", "CqxjzfudGAc", "goH-9MfQI7w", "KYIdr_7H5Yw", "J1WeqmGbYeI"};
|
||||||
|
|
||||||
|
final String encryptedUrl = "https://r6---sn-4g5ednek.googlevideo.com/videoplayback?expire=1626562120&ei=6AnzYO_YBpql1gLGkb_IBQ&ip=127.0.0.1&id=o-ANhBEf36Z5h-8U9DDddtPDqtS0ZNwf0XJAAigudKI2uI&itag=278&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&vprv=1&mime=video%2Fwebm&ns=TvecOReN0vPuXb3j_zq157IG&gir=yes&clen=2915100&dur=270.203&lmt=1608157174907785&keepalive=yes&fexp=24001373,24007246&c=WEB&txp=5535432&n=N9BWSTFT7vvBJrvQ&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&alr=yes&sig=AOq0QJ8wRQIgW6XnUDKPDSxiT0_KE_tDDMpcaCJl2Un5p0Fu9qZNQGkCIQDWxsDHi_s2BEmRqIbd1C5g_gzfihB7RZLsScKWNMwzzA%3D%3D&cpn=9r2yt3BqcYmeb2Yu&cver=2.20210716.00.00&redirect_counter=1&cm2rm=sn-4g5ezy7s&cms_redirect=yes&mh=Y5&mm=34&mn=sn-4g5ednek&ms=ltu&mt=1626540524&mv=m&mvi=6&pl=43&lsparams=mh,mm,mn,ms,mv,mvi,pl&lsig=AG3C_xAwRQIhAIUzxTn9Vw1-vm-_7OQ5-0h1M6AZsY9Bx1FlCCTeMICzAiADtGggbn4Znsrh2EnvyOsGnYdRGcbxn4mW9JMOQiInDQ%3D%3D&range=259165-480735&rn=11&rbuf=20190";
|
||||||
|
|
||||||
|
for (final String videoId : videoIds) {
|
||||||
|
try {
|
||||||
|
final String decryptedUrl = new YoutubeThrottlingDecrypter(videoId).apply(encryptedUrl);
|
||||||
|
assertNotEquals(encryptedUrl, decryptedUrl);
|
||||||
|
} catch (EvaluatorException e) {
|
||||||
|
fail("Failed to extract n param decrypt function for video " + videoId + "\n" + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDecode__success() throws ParsingException {
|
public void testDecode__success() throws ParsingException {
|
||||||
// URL extracted from browser with the dev tools
|
// URL extracted from browser with the dev tools
|
||||||
|
@ -17,7 +17,7 @@ import javax.annotation.Nullable;
|
|||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
|
|
||||||
// Doesn't work with mocks. Makes request with different `dataToSend` i think
|
// Doesn't work with mocks. Makes request with different `dataToSend` I think
|
||||||
public class YoutubeMusicSearchExtractorTest {
|
public class YoutubeMusicSearchExtractorTest {
|
||||||
public static class MusicSongs extends DefaultSearchExtractorTest {
|
public static class MusicSongs extends DefaultSearchExtractorTest {
|
||||||
private static SearchExtractor extractor;
|
private static SearchExtractor extractor;
|
||||||
@ -133,6 +133,7 @@ public class YoutubeMusicSearchExtractorTest {
|
|||||||
public static class Suggestion extends DefaultSearchExtractorTest {
|
public static class Suggestion extends DefaultSearchExtractorTest {
|
||||||
private static SearchExtractor extractor;
|
private static SearchExtractor extractor;
|
||||||
private static final String QUERY = "megaman x3";
|
private static final String QUERY = "megaman x3";
|
||||||
|
private static final boolean CORRECTED = true;
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
@ -150,6 +151,7 @@ public class YoutubeMusicSearchExtractorTest {
|
|||||||
@Override public String expectedSearchString() { return QUERY; }
|
@Override public String expectedSearchString() { return QUERY; }
|
||||||
@Nullable @Override public String expectedSearchSuggestion() { return "mega man x3"; }
|
@Nullable @Override public String expectedSearchSuggestion() { return "mega man x3"; }
|
||||||
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
|
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
|
||||||
|
@Override public boolean isCorrectedSearch() { return CORRECTED; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CorrectedSearch extends DefaultSearchExtractorTest {
|
public static class CorrectedSearch extends DefaultSearchExtractorTest {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.schabi.newpipe.extractor.services.youtube.search;
|
package org.schabi.newpipe.extractor.services.youtube.search;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.downloader.DownloaderFactory;
|
import org.schabi.newpipe.downloader.DownloaderFactory;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
@ -272,13 +273,14 @@ public class YoutubeSearchExtractorTest {
|
|||||||
urlTexts
|
urlTexts
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// testMoreRelatedItems is broken because a video has no duration shown
|
||||||
|
@Override public void testMoreRelatedItems() { }
|
||||||
@Override public SearchExtractor extractor() { return extractor; }
|
@Override public SearchExtractor extractor() { return extractor; }
|
||||||
@Override public StreamingService expectedService() { return YouTube; }
|
@Override public StreamingService expectedService() { return YouTube; }
|
||||||
@Override public String expectedName() { return QUERY; }
|
@Override public String expectedName() { return QUERY; }
|
||||||
@Override public String expectedId() { return QUERY; }
|
@Override public String expectedId() { return QUERY; }
|
||||||
@Override public String expectedUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
|
@Override public String expectedUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
|
||||||
@Override public String expectedOriginalUrlContains() throws Exception { return "youtube.com/results?search_query=" + QUERY; }
|
@Override public String expectedOriginalUrlContains() throws Exception { return "youtube.com/results?search_query=" + QUERY; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ChannelVerified extends DefaultSearchExtractorTest {
|
public static class ChannelVerified extends DefaultSearchExtractorTest {
|
||||||
@ -318,5 +320,4 @@ public class YoutubeSearchExtractorTest {
|
|||||||
assertTrue(verified);
|
assertTrue(verified);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
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.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
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;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ public class YoutubeStreamExtractorAgeRestrictedTest extends DefaultStreamExtrac
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "ageRestricted"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "ageRestricted"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -54,10 +56,10 @@ public class YoutubeStreamExtractorAgeRestrictedTest extends DefaultStreamExtrac
|
|||||||
@Override public long expectedDislikeCountAtLeast() { return 38000; }
|
@Override public long expectedDislikeCountAtLeast() { return 38000; }
|
||||||
@Override public boolean expectedHasRelatedItems() { return false; } // no related videos (!)
|
@Override public boolean expectedHasRelatedItems() { return false; } // no related videos (!)
|
||||||
@Override public int expectedAgeLimit() { return 18; }
|
@Override public int expectedAgeLimit() { return 18; }
|
||||||
@Nullable @Override public String expectedErrorMessage() { return "Sign in to confirm your age"; }
|
|
||||||
@Override public boolean expectedHasSubtitles() { return false; }
|
@Override public boolean expectedHasSubtitles() { return false; }
|
||||||
|
|
||||||
@Override public String expectedCategory() { return ""; } // Unavailable on age restricted videos
|
@Override public String expectedCategory() { return "Entertainment"; }
|
||||||
|
|
||||||
@Override public String expectedLicence() { return "YouTube licence"; }
|
@Override public String expectedLicence() { return "YouTube licence"; }
|
||||||
@Override
|
@Override
|
||||||
public List<String> expectedTags() {
|
public List<String> expectedTags() {
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
package org.schabi.newpipe.extractor.services.youtube.stream;
|
package org.schabi.newpipe.extractor.services.youtube.stream;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.schabi.newpipe.downloader.DownloaderFactory;
|
import org.schabi.newpipe.downloader.DownloaderFactory;
|
||||||
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.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.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
|
||||||
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.List;
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
|||||||
/**
|
/**
|
||||||
* Test for {@link YoutubeStreamLinkHandlerFactory}
|
* Test for {@link YoutubeStreamLinkHandlerFactory}
|
||||||
*/
|
*/
|
||||||
@Ignore("Video is not available in specific countries. Someone else has to generate mocks")
|
|
||||||
public class YoutubeStreamExtractorControversialTest extends DefaultStreamExtractorTest {
|
public class YoutubeStreamExtractorControversialTest extends DefaultStreamExtractorTest {
|
||||||
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/stream/";
|
private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/stream/";
|
||||||
private static final String ID = "T4XJQO3qol8";
|
private static final String ID = "T4XJQO3qol8";
|
||||||
@ -31,6 +32,8 @@ public class YoutubeStreamExtractorControversialTest extends DefaultStreamExtrac
|
|||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "controversial"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "controversial"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -59,5 +62,4 @@ public class YoutubeStreamExtractorControversialTest extends DefaultStreamExtrac
|
|||||||
@Override public List<String> expectedTags() { return Arrays.asList("Books", "Burning", "Jones", "Koran", "Qur'an", "Terry", "the amazing atheist"); }
|
@Override public List<String> expectedTags() { return Arrays.asList("Books", "Burning", "Jones", "Koran", "Qur'an", "Terry", "the amazing atheist"); }
|
||||||
@Override public String expectedCategory() { return "Entertainment"; }
|
@Override public String expectedCategory() { return "Entertainment"; }
|
||||||
@Override public String expectedLicence() { return "YouTube licence"; }
|
@Override public String expectedLicence() { return "YouTube licence"; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,7 @@ import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentExcepti
|
|||||||
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.services.youtube.extractors.YoutubeStreamExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.*;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
@ -66,6 +63,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
public static void setUp() throws IOException {
|
public static void setUp() throws IOException {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "notAvailable"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "notAvailable"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +120,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "pewdiwpie"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "pewdiwpie"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -165,6 +164,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "unboxing"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "unboxing"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -218,6 +218,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "ratingsDisabled"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "ratingsDisabled"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -255,6 +256,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "streamSegmentsTagesschau"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "streamSegmentsTagesschau"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -316,6 +318,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "streamSegmentsMaiLab"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "streamSegmentsMaiLab"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -386,6 +389,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "publicBroadcast"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "publicBroadcast"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,6 +439,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance());
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
extractor = (YoutubeStreamExtractor) YouTube
|
extractor = (YoutubeStreamExtractor) YouTube
|
||||||
.getStreamExtractor("https://www.youtube.com/watch?v=tjz2u2DiveM");
|
.getStreamExtractor("https://www.youtube.com/watch?v=tjz2u2DiveM");
|
||||||
@ -454,6 +459,7 @@ public class YoutubeStreamExtractorDefaultTest {
|
|||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance());
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package org.schabi.newpipe.extractor.services.youtube.stream;
|
package org.schabi.newpipe.extractor.services.youtube.stream;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.downloader.DownloaderFactory;
|
import org.schabi.newpipe.downloader.DownloaderFactory;
|
||||||
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.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.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
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;
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ public class YoutubeStreamExtractorLivestreamTest extends DefaultStreamExtractor
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "live"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "live"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
@ -37,7 +38,6 @@ public class YoutubeStreamExtractorLivestreamTest extends DefaultStreamExtractor
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Test
|
@Test
|
||||||
@Ignore("When visiting website it shows 'Lofi Girl', unknown why it's different in tests")
|
|
||||||
public void testUploaderName() throws Exception {
|
public void testUploaderName() throws Exception {
|
||||||
super.testUploaderName();
|
super.testUploaderName();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
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.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
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;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ public class YoutubeStreamExtractorUnlistedTest extends DefaultStreamExtractorTe
|
|||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
YoutubeParsingHelper.resetClientVersionAndKey();
|
YoutubeParsingHelper.resetClientVersionAndKey();
|
||||||
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
YoutubeParsingHelper.setNumberGenerator(new Random(1));
|
||||||
|
YoutubeStreamExtractor.resetDeobfuscationCode();
|
||||||
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "unlisted"));
|
NewPipe.init(new DownloaderFactory().getDownloader(RESOURCE_PATH + "unlisted"));
|
||||||
extractor = YouTube.getStreamExtractor(URL);
|
extractor = YouTube.getStreamExtractor(URL);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
package org.schabi.newpipe.extractor.utils;
|
||||||
|
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.StringUtils.matchToClosingParenthesis;
|
||||||
|
|
||||||
|
public class StringUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualDecodeFunction__success() {
|
||||||
|
String preNoise = "if(\"function\"===typeof b&&\"function\"===typeof c||\"function\"===typeof c&&\"function\"===typeof d)throw Error(\"It looks like you are passing several store enhancers to createStore(). This is not supported. Instead, compose them together to a single function.\");\"function\"===typeof b&&\"undefined\"===typeof c&&(c=b,b=void 0);if(\"undefined\"!==typeof c){if(\"function\"!==typeof c)throw Error(\"Expected the enhancer to be a function.\");return c(Dr)(a,b)}if(\"function\"!==typeof a)throw Error(\"Expected the reducer to be a function.\");\n" +
|
||||||
|
"var l=a,m=b,n=[],p=n,q=!1;h({type:Cr});a={};var t=(a.dispatch=h,a.subscribe=f,a.getState=e,a.replaceReducer=function(u){if(\"function\"!==typeof u)throw Error(\"Expected the nextReducer to be a function.\");l=u;h({type:hha});return t},a[Er]=function(){var u={};\n" +
|
||||||
|
"return u.subscribe=function(x){function y(){x.next&&x.next(e())}\n" +
|
||||||
|
"if(\"object\"!==typeof x||null===x)throw new TypeError(\"Expected the observer to be an object.\");y();return{unsubscribe:f(y)}},u[Er]=function(){return this},u},a);\n" +
|
||||||
|
"return t};\n" +
|
||||||
|
"Fr=function(a){De.call(this,a,-1,iha)};\n" +
|
||||||
|
"Gr=function(a){De.call(this,a)};\n" +
|
||||||
|
"jha=function(a,b){for(;Jd(b);)switch(b.C){case 10:var c=Od(b);Ge(a,1,c);break;case 18:c=Od(b);Ge(a,2,c);break;case 26:c=Od(b);Ge(a,3,c);break;case 34:c=Od(b);Ge(a,4,c);break;case 40:c=Hd(b.i);Ge(a,5,c);break;default:if(!we(b))return a}return a};";
|
||||||
|
String signature = "kha=function(a)";
|
||||||
|
String body = "{var b=a.split(\"\"),c=[-1186681497,-1653318181,372630254,function(d,e){for(var f=64,h=[];++f-h.length-32;){switch(f){case 58:f-=14;case 91:case 92:case 93:continue;case 123:f=47;case 94:case 95:case 96:continue;case 46:f=95}h.push(String.fromCharCode(f))}d.forEach(function(l,m,n){this.push(n[m]=h[(h.indexOf(l)-h.indexOf(this[m])+m-32+f--)%h.length])},e.split(\"\"))},\n" +
|
||||||
|
"-467738125,1158037010,function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f},\n" +
|
||||||
|
"\"continue\",158531598,-172776392,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},\n" +
|
||||||
|
"-1753359936,function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])},\n" +
|
||||||
|
"1533713399,-1736576025,-1274201783,function(d){d.reverse()},\n" +
|
||||||
|
"169126570,1077517431,function(d,e){d.push(e)},\n" +
|
||||||
|
"-1807932259,-150219E3,480561184,-3495188,-1856307605,1416497372,b,-1034568435,-501230371,1979778585,null,b,-1049521459,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},\n" +
|
||||||
|
"1119056651,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())},\n" +
|
||||||
|
"b,1460920438,135616752,-1807932259,-815823682,-387465417,1979778585,113585E4,function(d,e){d.push(e)},\n" +
|
||||||
|
"-1753359936,-241651400,-386043301,-144139513,null,null,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)}];\n" +
|
||||||
|
"c[30]=c;c[49]=c;c[50]=c;try{c[51](c[26],c[25]),c[10](c[30],c[17]),c[5](c[28],c[9]),c[18](c[51]),c[14](c[19],c[21]),c[8](c[40],c[22]),c[50](c[35],c[28]),c[24](c[29],c[3]),c[0](c[31],c[19]),c[27](c[26],c[33]),c[29](c[36],c[40]),c[50](c[26]),c[27](c[32],c[9]),c[8](c[10],c[14]),c[35](c[44],c[28]),c[22](c[44],c[1]),c[8](c[11],c[3]),c[29](c[44]),c[21](c[41],c[45]),c[16](c[32],c[4]),c[17](c[14],c[26]),c[36](c[20],c[45]),c[43](c[35],c[39]),c[43](c[20],c[23]),c[43](c[10],c[51]),c[43](c[34],c[32]),c[29](c[34],\n" +
|
||||||
|
"c[49]),c[43](c[20],c[44]),c[49](c[20]),c[19](c[15],c[8]),c[36](c[15],c[46]),c[17](c[20],c[37]),c[18](c[10]),c[17](c[34],c[31]),c[19](c[10],c[30]),c[19](c[20],c[2]),c[36](c[20],c[21]),c[43](c[35],c[16]),c[19](c[35],c[5]),c[18](c[46],c[34])}catch(d){return\"enhanced_except_lJMB6-z-_w8_\"+a}return b.join(\"\")}";
|
||||||
|
String postNoise = "Hr=function(a){this.i=a}";
|
||||||
|
|
||||||
|
String substring = matchToClosingParenthesis(preNoise + '\n' + signature + body + ";" + postNoise, signature);
|
||||||
|
|
||||||
|
assertEquals(body, substring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void moreClosing__success() {
|
||||||
|
String expected = "{{{}}}";
|
||||||
|
String string = "a" + expected + "}}";
|
||||||
|
|
||||||
|
String substring = matchToClosingParenthesis(string, "a");
|
||||||
|
|
||||||
|
assertEquals(expected, substring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Functionality currently not needed")
|
||||||
|
@Test
|
||||||
|
public void lessClosing__success() {
|
||||||
|
String expected = "{{{}}}";
|
||||||
|
String string = "a{{" + expected;
|
||||||
|
|
||||||
|
String substring = matchToClosingParenthesis(string, "a");
|
||||||
|
|
||||||
|
assertEquals(expected, substring);
|
||||||
|
}
|
||||||
|
}
|
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
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
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
@ -1,62 +1,244 @@
|
|||||||
{
|
{
|
||||||
"request": {
|
"request": {
|
||||||
"httpMethod": "GET",
|
"httpMethod": "POST",
|
||||||
"url": "https://www.youtube.com/channel/DOESNT-EXIST/videos?pbj\u003d1\u0026view\u003d0\u0026flow\u003dgrid",
|
"url": "https://www.youtube.com/youtubei/v1/browse?key\u003dAIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Accept-Language": [
|
"Accept-Language": [
|
||||||
"en-GB, en;q\u003d0.9"
|
"en-GB, en;q\u003d0.9"
|
||||||
],
|
],
|
||||||
"Cookie": [
|
"Origin": [
|
||||||
"CONSENT\u003dPENDING+506"
|
"https://www.youtube.com"
|
||||||
],
|
],
|
||||||
"X-YouTube-Client-Name": [
|
"X-YouTube-Client-Name": [
|
||||||
"1"
|
"1"
|
||||||
],
|
],
|
||||||
|
"Referer": [
|
||||||
|
"https://www.youtube.com"
|
||||||
|
],
|
||||||
"X-YouTube-Client-Version": [
|
"X-YouTube-Client-Version": [
|
||||||
"2.20200214.04.00"
|
"2.20210728.00.00"
|
||||||
|
],
|
||||||
|
"Content-Type": [
|
||||||
|
"application/json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"dataToSend": [
|
||||||
|
123,
|
||||||
|
34,
|
||||||
|
98,
|
||||||
|
114,
|
||||||
|
111,
|
||||||
|
119,
|
||||||
|
115,
|
||||||
|
101,
|
||||||
|
73,
|
||||||
|
100,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
34,
|
||||||
|
68,
|
||||||
|
79,
|
||||||
|
69,
|
||||||
|
83,
|
||||||
|
78,
|
||||||
|
84,
|
||||||
|
45,
|
||||||
|
69,
|
||||||
|
88,
|
||||||
|
73,
|
||||||
|
83,
|
||||||
|
84,
|
||||||
|
34,
|
||||||
|
44,
|
||||||
|
34,
|
||||||
|
99,
|
||||||
|
111,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
101,
|
||||||
|
120,
|
||||||
|
116,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
123,
|
||||||
|
34,
|
||||||
|
99,
|
||||||
|
108,
|
||||||
|
105,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
123,
|
||||||
|
34,
|
||||||
|
104,
|
||||||
|
108,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
34,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
45,
|
||||||
|
71,
|
||||||
|
66,
|
||||||
|
34,
|
||||||
|
44,
|
||||||
|
34,
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
34,
|
||||||
|
71,
|
||||||
|
66,
|
||||||
|
34,
|
||||||
|
44,
|
||||||
|
34,
|
||||||
|
99,
|
||||||
|
108,
|
||||||
|
105,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
78,
|
||||||
|
97,
|
||||||
|
109,
|
||||||
|
101,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
34,
|
||||||
|
87,
|
||||||
|
69,
|
||||||
|
66,
|
||||||
|
34,
|
||||||
|
44,
|
||||||
|
34,
|
||||||
|
99,
|
||||||
|
108,
|
||||||
|
105,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
86,
|
||||||
|
101,
|
||||||
|
114,
|
||||||
|
115,
|
||||||
|
105,
|
||||||
|
111,
|
||||||
|
110,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
34,
|
||||||
|
50,
|
||||||
|
46,
|
||||||
|
50,
|
||||||
|
48,
|
||||||
|
50,
|
||||||
|
49,
|
||||||
|
48,
|
||||||
|
55,
|
||||||
|
50,
|
||||||
|
56,
|
||||||
|
46,
|
||||||
|
48,
|
||||||
|
48,
|
||||||
|
46,
|
||||||
|
48,
|
||||||
|
48,
|
||||||
|
34,
|
||||||
|
125,
|
||||||
|
44,
|
||||||
|
34,
|
||||||
|
117,
|
||||||
|
115,
|
||||||
|
101,
|
||||||
|
114,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
123,
|
||||||
|
34,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
99,
|
||||||
|
107,
|
||||||
|
101,
|
||||||
|
100,
|
||||||
|
83,
|
||||||
|
97,
|
||||||
|
102,
|
||||||
|
101,
|
||||||
|
116,
|
||||||
|
121,
|
||||||
|
77,
|
||||||
|
111,
|
||||||
|
100,
|
||||||
|
101,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
102,
|
||||||
|
97,
|
||||||
|
108,
|
||||||
|
115,
|
||||||
|
101,
|
||||||
|
125,
|
||||||
|
125,
|
||||||
|
44,
|
||||||
|
34,
|
||||||
|
112,
|
||||||
|
97,
|
||||||
|
114,
|
||||||
|
97,
|
||||||
|
109,
|
||||||
|
115,
|
||||||
|
34,
|
||||||
|
58,
|
||||||
|
34,
|
||||||
|
69,
|
||||||
|
103,
|
||||||
|
90,
|
||||||
|
50,
|
||||||
|
97,
|
||||||
|
87,
|
||||||
|
82,
|
||||||
|
108,
|
||||||
|
98,
|
||||||
|
51,
|
||||||
|
77,
|
||||||
|
37,
|
||||||
|
51,
|
||||||
|
68,
|
||||||
|
34,
|
||||||
|
125
|
||||||
|
],
|
||||||
"localization": {
|
"localization": {
|
||||||
"languageCode": "en",
|
"languageCode": "en",
|
||||||
"countryCode": "GB"
|
"countryCode": "GB"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"responseCode": 404,
|
"responseCode": 400,
|
||||||
"responseMessage": "",
|
"responseMessage": "",
|
||||||
"responseHeaders": {
|
"responseHeaders": {
|
||||||
"alt-svc": [
|
"alt-svc": [
|
||||||
"h3\u003d\":443\"; ma\u003d2592000,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\u003d\":443\"; ma\u003d2592000,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": [
|
"cache-control": [
|
||||||
"no-cache, no-store, max-age\u003d0, must-revalidate"
|
"private"
|
||||||
],
|
],
|
||||||
"content-type": [
|
"content-type": [
|
||||||
"text/html; charset\u003dutf-8"
|
"application/json; charset\u003dUTF-8"
|
||||||
],
|
],
|
||||||
"date": [
|
"date": [
|
||||||
"Sat, 03 Jul 2021 11:29:58 GMT"
|
"Fri, 30 Jul 2021 17:13:39 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.\""
|
|
||||||
],
|
|
||||||
"permissions-policy": [
|
|
||||||
"ch-ua-full-version\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*, ch-ua-arch\u003d*, ch-ua-model\u003d*"
|
|
||||||
],
|
|
||||||
"pragma": [
|
|
||||||
"no-cache"
|
|
||||||
],
|
],
|
||||||
"server": [
|
"server": [
|
||||||
"ESF"
|
"ESF"
|
||||||
],
|
],
|
||||||
"set-cookie": [
|
"vary": [
|
||||||
"YSC\u003dotZ2jZ94DRk; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
|
"Origin",
|
||||||
],
|
"X-Origin",
|
||||||
"strict-transport-security": [
|
"Referer"
|
||||||
"max-age\u003d31536000"
|
|
||||||
],
|
],
|
||||||
"x-content-type-options": [
|
"x-content-type-options": [
|
||||||
"nosniff"
|
"nosniff"
|
||||||
@ -68,7 +250,7 @@
|
|||||||
"0"
|
"0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"responseBody": "\u003chtml lang\u003d\"en-GB\" dir\u003d\"ltr\"\u003e\u003chead\u003e\u003ctitle\u003e404 Not Found\u003c/title\u003e\u003cstyle nonce\u003d\"n/vBxRiZa1jxbE1/ttyVwQ\"\u003e*{margin:0;padding:0;border:0}html,body{height:100%;}\u003c/style\u003e\u003clink rel\u003d\"shortcut icon\" href\u003d\"https://www.youtube.com/img/favicon.ico\" type\u003d\"image/x-icon\"\u003e\u003clink rel\u003d\"icon\" href\u003d\"https://www.youtube.com/img/favicon_32.png\" sizes\u003d\"32x32\"\u003e\u003clink rel\u003d\"icon\" href\u003d\"https://www.youtube.com/img/favicon_48.png\" sizes\u003d\"48x48\"\u003e\u003clink rel\u003d\"icon\" href\u003d\"https://www.youtube.com/img/favicon_96.png\" sizes\u003d\"96x96\"\u003e\u003clink rel\u003d\"icon\" href\u003d\"https://www.youtube.com/img/favicon_144.png\" sizes\u003d\"144x144\"\u003e\u003c/head\u003e\u003cbody\u003e\u003ciframe style\u003d\"display:block;border:0;\" src\u003d\"/error?src\u003d404\u0026amp;ifr\u003d1\u0026amp;error\u003d\" width\u003d\"100%\" height\u003d\"100%\" frameborder\u003d\"\\\" scrolling\u003d\"no\"\u003e\u003c/iframe\u003e\u003c/body\u003e\u003c/html\u003e",
|
"responseBody": "{\n \"error\": {\n \"code\": 400,\n \"message\": \"Request contains an invalid argument.\",\n \"errors\": [\n {\n \"message\": \"Request contains an invalid argument.\",\n \"domain\": \"global\",\n \"reason\": \"badRequest\"\n }\n ],\n \"status\": \"INVALID_ARGUMENT\"\n }\n}\n",
|
||||||
"latestUrl": "https://www.youtube.com/channel/DOESNT-EXIST/videos?pbj\u003d1\u0026view\u003d0\u0026flow\u003dgrid"
|
"latestUrl": "https://www.youtube.com/youtubei/v1/browse?key\u003dAIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||||
}
|
}
|
||||||
}
|
}
|
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
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
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user