Merge pull request #701 from Stypox/v0.21.8

V0.21.8
This commit is contained in:
Stypox 2021-08-03 20:43:15 +02:00 committed by GitHub
commit bb3815d19b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
264 changed files with 48802 additions and 15883 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More