kopia lustrzana https://github.com/TeamNewPipe/NewPipeExtractor
Merge remote-tracking branch 'origin/dev' into bandcamp
commit
116e921d6c
|
@ -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.20.3'`the `dependencies` in your `build.gradle`. Replace `v0.20.3` with the latest release.
|
2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.20.6'`the `dependencies` in your `build.gradle`. Replace `v0.20.6` 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.
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ The following sites are currently supported:
|
||||||
|
|
||||||
- YouTube
|
- YouTube
|
||||||
- SoundCloud
|
- SoundCloud
|
||||||
- MediaCCC
|
- media.ccc.de
|
||||||
- PeerTube (no P2P)
|
- PeerTube (no P2P)
|
||||||
- Bandcamp
|
- Bandcamp
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ allprojects {
|
||||||
sourceCompatibility = 1.8
|
sourceCompatibility = 1.8
|
||||||
targetCompatibility = 1.8
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
version 'v0.20.3'
|
version 'v0.20.5'
|
||||||
group 'com.github.TeamNewPipe'
|
group 'com.github.TeamNewPipe'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public abstract class Extractor {
|
public abstract class Extractor {
|
||||||
/**
|
/**
|
||||||
|
@ -29,12 +30,9 @@ public abstract class Extractor {
|
||||||
private final Downloader downloader;
|
private final Downloader downloader;
|
||||||
|
|
||||||
public Extractor(final StreamingService service, final LinkHandler linkHandler) {
|
public Extractor(final StreamingService service, final LinkHandler linkHandler) {
|
||||||
if (service == null) throw new NullPointerException("service is null");
|
this.service = Objects.requireNonNull(service, "service is null");
|
||||||
if (linkHandler == null) throw new NullPointerException("LinkHandler is null");
|
this.linkHandler = Objects.requireNonNull(linkHandler, "LinkHandler is null");
|
||||||
this.service = service;
|
this.downloader = Objects.requireNonNull(NewPipe.getDownloader(), "downloader is null");
|
||||||
this.linkHandler = linkHandler;
|
|
||||||
this.downloader = NewPipe.getDownloader();
|
|
||||||
if (downloader == null) throw new NullPointerException("downloader is null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class Localization implements Serializable {
|
public class Localization implements Serializable {
|
||||||
public static final Localization DEFAULT = new Localization("en", "GB");
|
public static final Localization DEFAULT = new Localization("en", "GB");
|
||||||
|
@ -89,14 +90,14 @@ public class Localization implements Serializable {
|
||||||
|
|
||||||
Localization that = (Localization) o;
|
Localization that = (Localization) o;
|
||||||
|
|
||||||
if (!languageCode.equals(that.languageCode)) return false;
|
return languageCode.equals(that.languageCode) &&
|
||||||
return countryCode != null ? countryCode.equals(that.countryCode) : that.countryCode == null;
|
Objects.equals(countryCode, that.countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = languageCode.hashCode();
|
int result = languageCode.hashCode();
|
||||||
result = 31 * result + (countryCode != null ? countryCode.hashCode() : 0);
|
result = 31 * result + Objects.hashCode(countryCode);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCap
|
||||||
|
|
||||||
public class MediaCCCService extends StreamingService {
|
public class MediaCCCService extends StreamingService {
|
||||||
public MediaCCCService(final int id) {
|
public MediaCCCService(final int id) {
|
||||||
super(id, "MediaCCC", asList(AUDIO, VIDEO));
|
super(id, "media.ccc.de", asList(AUDIO, VIDEO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems;
|
package org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
@ -23,7 +25,7 @@ public class MediaCCCConferenceInfoItemExtractor implements ChannelInfoItemExtra
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getStreamCount() {
|
public long getStreamCount() {
|
||||||
return -1;
|
return ListExtractor.ITEM_COUNT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -5,8 +5,10 @@ import com.grack.nanojson.JsonObject;
|
||||||
import com.grack.nanojson.JsonParser;
|
import com.grack.nanojson.JsonParser;
|
||||||
import com.grack.nanojson.JsonParserException;
|
import com.grack.nanojson.JsonParserException;
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
|
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.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
@ -21,6 +23,7 @@ import java.io.UnsupportedEncodingException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
|
@ -35,6 +38,7 @@ import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.HTTP;
|
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.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.
|
||||||
|
@ -110,20 +114,18 @@ public class YoutubeParsingHelper {
|
||||||
return host.equalsIgnoreCase("invidio.us")
|
return host.equalsIgnoreCase("invidio.us")
|
||||||
|| host.equalsIgnoreCase("dev.invidio.us")
|
|| host.equalsIgnoreCase("dev.invidio.us")
|
||||||
|| host.equalsIgnoreCase("www.invidio.us")
|
|| host.equalsIgnoreCase("www.invidio.us")
|
||||||
|
|| host.equalsIgnoreCase("vid.encryptionin.space")
|
||||||
|| host.equalsIgnoreCase("invidious.snopyta.org")
|
|| host.equalsIgnoreCase("invidious.snopyta.org")
|
||||||
|| host.equalsIgnoreCase("fi.invidious.snopyta.org")
|
|
||||||
|| host.equalsIgnoreCase("yewtu.be")
|
|| host.equalsIgnoreCase("yewtu.be")
|
||||||
|| host.equalsIgnoreCase("invidious.ggc-project.de")
|
|| host.equalsIgnoreCase("tube.connect.cafe")
|
||||||
|| host.equalsIgnoreCase("yt.maisputain.ovh")
|
|| host.equalsIgnoreCase("invidious.zapashcanon.fr")
|
||||||
|| host.equalsIgnoreCase("invidious.13ad.de")
|
|| host.equalsIgnoreCase("invidious.kavin.rocks")
|
||||||
|| host.equalsIgnoreCase("invidious.toot.koeln")
|
|| host.equalsIgnoreCase("invidious.tube")
|
||||||
|| host.equalsIgnoreCase("invidious.fdn.fr")
|
|| host.equalsIgnoreCase("invidious.site")
|
||||||
|| host.equalsIgnoreCase("watch.nettohikari.com")
|
|| host.equalsIgnoreCase("invidious.xyz")
|
||||||
|| host.equalsIgnoreCase("invidious.snwmds.net")
|
|| host.equalsIgnoreCase("vid.mint.lgbt")
|
||||||
|| host.equalsIgnoreCase("invidious.snwmds.org")
|
|| host.equalsIgnoreCase("invidiou.site")
|
||||||
|| host.equalsIgnoreCase("invidious.snwmds.com")
|
|| host.equalsIgnoreCase("invidious.fdn.fr");
|
||||||
|| host.equalsIgnoreCase("invidious.sunsetravens.com")
|
|
||||||
|| host.equalsIgnoreCase("invidious.gachirangers.com");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -194,6 +196,57 @@ public class YoutubeParsingHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given playlist id is a YouTube Mix (auto-generated playlist)
|
||||||
|
* Ids from a YouTube Mix start with "RD"
|
||||||
|
* @param playlistId
|
||||||
|
* @return Whether given id belongs to a YouTube Mix
|
||||||
|
*/
|
||||||
|
public static boolean isYoutubeMixId(final String playlistId) {
|
||||||
|
return playlistId.startsWith("RD") && !isYoutubeMusicMixId(playlistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given playlist id is a YouTube Music Mix (auto-generated playlist)
|
||||||
|
* Ids from a YouTube Music Mix start with "RDAMVM"
|
||||||
|
* @param playlistId
|
||||||
|
* @return Whether given id belongs to a YouTube Music Mix
|
||||||
|
*/
|
||||||
|
public static boolean isYoutubeMusicMixId(final String playlistId) {
|
||||||
|
return playlistId.startsWith("RDAMVM");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks if the given playlist id is a YouTube Channel Mix (auto-generated playlist)
|
||||||
|
* Ids from a YouTube channel Mix start with "RDCM"
|
||||||
|
* @return Whether given id belongs to a YouTube Channel Mix
|
||||||
|
*/
|
||||||
|
public static boolean isYoutubeChannelMixId(final String playlistId) {
|
||||||
|
return playlistId.startsWith("RDCM");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the video id from the playlist id for Mixes.
|
||||||
|
* @throws ParsingException If the playlistId is a Channel Mix or not a mix.
|
||||||
|
*/
|
||||||
|
public static String extractVideoIdFromMixId(final String playlistId) throws ParsingException {
|
||||||
|
if (playlistId.startsWith("RDMM")) { //My Mix
|
||||||
|
return playlistId.substring(4);
|
||||||
|
|
||||||
|
} else if (playlistId.startsWith("RDAMVM")) { //Music mix
|
||||||
|
return playlistId.substring(6);
|
||||||
|
|
||||||
|
} else if (playlistId.startsWith("RMCM")) { //Channel mix
|
||||||
|
//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);
|
||||||
|
|
||||||
|
} else if (playlistId.startsWith("RD")) { // Normal mix
|
||||||
|
return playlistId.substring(2);
|
||||||
|
|
||||||
|
} else { //not a mix
|
||||||
|
throw new ParsingException("Video id could not be determined from mix id: " + playlistId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static JsonObject getInitialData(String html) throws ParsingException {
|
public static JsonObject getInitialData(String html) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
|
@ -418,10 +471,14 @@ public class YoutubeParsingHelper {
|
||||||
} 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").getString("playlistId"));
|
url.append("&list=").append(navigationEndpoint.getObject("watchEndpoint")
|
||||||
if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds"))
|
.getString("playlistId"));
|
||||||
url.append("&t=").append(navigationEndpoint.getObject("watchEndpoint").getInt("startTimeSeconds"));
|
}
|
||||||
|
if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds")) {
|
||||||
|
url.append("&t=").append(navigationEndpoint.getObject("watchEndpoint")
|
||||||
|
.getInt("startTimeSeconds"));
|
||||||
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
} else if (navigationEndpoint.has("watchPlaylistEndpoint")) {
|
} else if (navigationEndpoint.has("watchPlaylistEndpoint")) {
|
||||||
return "https://www.youtube.com/playlist?list=" +
|
return "https://www.youtube.com/playlist?list=" +
|
||||||
|
@ -487,8 +544,8 @@ public class YoutubeParsingHelper {
|
||||||
public static String getValidJsonResponseBody(final Response response)
|
public static String getValidJsonResponseBody(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"
|
||||||
" (\"" + response.responseCode() + " " + response.responseMessage() + "\")");
|
+ " (\"" + response.responseCode() + " " + response.responseMessage() + "\")");
|
||||||
}
|
}
|
||||||
|
|
||||||
final String responseBody = response.responseBody();
|
final String responseBody = response.responseBody();
|
||||||
|
@ -508,13 +565,39 @@ public class YoutubeParsingHelper {
|
||||||
final String responseContentType = response.getHeader("Content-Type");
|
final String responseContentType = response.getHeader("Content-Type");
|
||||||
if (responseContentType != null
|
if (responseContentType != null
|
||||||
&& responseContentType.toLowerCase().contains("text/html")) {
|
&& responseContentType.toLowerCase().contains("text/html")) {
|
||||||
throw new ParsingException("Got HTML document, expected JSON response" +
|
throw new ParsingException("Got HTML document, expected JSON response"
|
||||||
" (latest url was: \"" + response.latestUrl() + "\")");
|
+ " (latest url was: \"" + response.latestUrl() + "\")");
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Response getResponse(final String url, final Localization localization)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
headers.put("X-YouTube-Client-Name", Collections.singletonList("1"));
|
||||||
|
headers.put("X-YouTube-Client-Version", Collections.singletonList(getClientVersion()));
|
||||||
|
|
||||||
|
final Response response = getDownloader().get(url, headers, localization);
|
||||||
|
getValidJsonResponseBody(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String extractCookieValue(final String cookieName, final Response response) {
|
||||||
|
final List<String> cookies = response.responseHeaders().get("set-cookie");
|
||||||
|
int startIndex;
|
||||||
|
String result = "";
|
||||||
|
for (final String cookie : cookies) {
|
||||||
|
startIndex = cookie.indexOf(cookieName);
|
||||||
|
if (startIndex != -1) {
|
||||||
|
result = cookie.substring(startIndex + cookieName.length() + "=".length(),
|
||||||
|
cookie.indexOf(";", startIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
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<>();
|
||||||
|
@ -522,8 +605,24 @@ public class YoutubeParsingHelper {
|
||||||
headers.put("X-YouTube-Client-Version", Collections.singletonList(getClientVersion()));
|
headers.put("X-YouTube-Client-Version", Collections.singletonList(getClientVersion()));
|
||||||
final Response response = getDownloader().get(url, headers, localization);
|
final Response response = getDownloader().get(url, headers, localization);
|
||||||
|
|
||||||
final String responseBody = getValidJsonResponseBody(response);
|
return toJsonArray(getValidJsonResponseBody(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonArray getJsonResponse(final Page page, final Localization localization)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final Map<String, List<String>> headers = new HashMap<>();
|
||||||
|
if (!isNullOrEmpty(page.getCookies())) {
|
||||||
|
headers.put("Cookie", Collections.singletonList(join(";", "=", page.getCookies())));
|
||||||
|
}
|
||||||
|
headers.put("X-YouTube-Client-Name", Collections.singletonList("1"));
|
||||||
|
headers.put("X-YouTube-Client-Version", Collections.singletonList(getClientVersion()));
|
||||||
|
|
||||||
|
final Response response = getDownloader().get(page.getUrl(), headers, localization);
|
||||||
|
|
||||||
|
return toJsonArray(getValidJsonResponseBody(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonArray toJsonArray(final String responseBody) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
return JsonParser.array().from(responseBody);
|
return JsonParser.array().from(responseBody);
|
||||||
} catch (JsonParserException e) {
|
} catch (JsonParserException e) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeChannelExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeChannelExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCommentsExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCommentsExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeFeedExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeFeedExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMixPlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMusicSearchExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMusicSearchExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubePlaylistExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubePlaylistExtractor;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSearchExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSearchExtractor;
|
||||||
|
@ -109,8 +110,12 @@ public class YoutubeService extends StreamingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler) {
|
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
|
||||||
return new YoutubePlaylistExtractor(this, linkHandler);
|
if (YoutubeParsingHelper.isYoutubeMixId(linkHandler.getId())) {
|
||||||
|
return new YoutubeMixPlaylistExtractor(this, linkHandler);
|
||||||
|
} else {
|
||||||
|
return new YoutubePlaylistExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.extractor.services.youtube.extractors;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
|
||||||
|
@ -86,7 +87,7 @@ public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor
|
||||||
try {
|
try {
|
||||||
if (!channelInfoItem.has("videoCountText")) {
|
if (!channelInfoItem.has("videoCountText")) {
|
||||||
// Video count is not available, channel probably has no public uploads.
|
// Video count is not available, channel probably has no public uploads.
|
||||||
return -1;
|
return ListExtractor.ITEM_COUNT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Long.parseLong(Utils.removeNonDigitCharacters(getTextFromObject(
|
return Long.parseLong(Utils.removeNonDigitCharacters(getTextFromObject(
|
||||||
|
|
|
@ -160,8 +160,15 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String findValue(String doc, String start, String end) {
|
private String findValue(String doc, String start, String end) {
|
||||||
final int beginIndex = doc.indexOf(start) + start.length();
|
final String unescaped = doc
|
||||||
final int endIndex = doc.indexOf(end, beginIndex);
|
.replaceAll("\\\\x22", "\"")
|
||||||
return doc.substring(beginIndex, endIndex);
|
.replaceAll("\\\\x7b", "{")
|
||||||
|
.replaceAll("\\\\x7d", "}")
|
||||||
|
.replaceAll("\\\\x5b", "[")
|
||||||
|
.replaceAll("\\\\x5d", "]");
|
||||||
|
|
||||||
|
final int beginIndex = unescaped.indexOf(start) + start.length();
|
||||||
|
final int endIndex = unescaped.indexOf(end, beginIndex);
|
||||||
|
return unescaped.substring(beginIndex, endIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.extractCookieValue;
|
||||||
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
||||||
|
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.services.youtube.YoutubeParsingHelper.toJsonArray;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link YoutubePlaylistExtractor} for a mix (auto-generated playlist).
|
||||||
|
* It handles URLs in the format of
|
||||||
|
* {@code youtube.com/watch?v=videoId&list=playlistId}
|
||||||
|
*/
|
||||||
|
public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTube identifies mixes based on this cookie. With this information it can generate
|
||||||
|
* continuations without duplicates.
|
||||||
|
*/
|
||||||
|
public static final String COOKIE_NAME = "VISITOR_INFO1_LIVE";
|
||||||
|
|
||||||
|
private JsonObject initialData;
|
||||||
|
private JsonObject playlistData;
|
||||||
|
private String cookieValue;
|
||||||
|
|
||||||
|
public YoutubeMixPlaylistExtractor(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final String url = getUrl() + "&pbj=1";
|
||||||
|
final Response response = getResponse(url, getExtractorLocalization());
|
||||||
|
final JsonArray ajaxJson = toJsonArray(response.responseBody());
|
||||||
|
initialData = ajaxJson.getObject(3).getObject("response");
|
||||||
|
playlistData = initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
||||||
|
.getObject("playlist").getObject("playlist");
|
||||||
|
cookieValue = extractCookieValue(COOKIE_NAME, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
final String name = playlistData.getString("title");
|
||||||
|
if (name == null) {
|
||||||
|
throw new ParsingException("Could not get playlist name");
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getThumbnailUrl() throws ParsingException {
|
||||||
|
try {
|
||||||
|
return getThumbnailUrlFromPlaylistId(playlistData.getString("playlistId"));
|
||||||
|
} catch (final Exception e) {
|
||||||
|
try {
|
||||||
|
//fallback to thumbnail of current video. Always the case for channel mix
|
||||||
|
return getThumbnailUrlFromVideoId(
|
||||||
|
initialData.getObject("currentVideoEndpoint").getObject("watchEndpoint")
|
||||||
|
.getString("videoId"));
|
||||||
|
} catch (final Exception ignored) {
|
||||||
|
}
|
||||||
|
throw new ParsingException("Could not get playlist thumbnail", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBannerUrl() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
//Youtube mix are auto-generated
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
//Youtube mix are auto-generated by YouTube
|
||||||
|
return "YouTube";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderAvatarUrl() {
|
||||||
|
//Youtube mix are auto-generated by YouTube
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() {
|
||||||
|
// Auto-generated playlist always start with 25 videos and are endless
|
||||||
|
return ListExtractor.ITEM_COUNT_INFINITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
|
||||||
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
collectStreamsFrom(collector, playlistData.getArray("contents"));
|
||||||
|
return new InfoItemsPage<>(collector,
|
||||||
|
new Page(getNextPageUrlFrom(playlistData), Collections.singletonMap(COOKIE_NAME, cookieValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNextPageUrlFrom(final JsonObject playlistJson) throws ExtractionException {
|
||||||
|
final JsonObject lastStream = ((JsonObject) playlistJson.getArray("contents")
|
||||||
|
.get(playlistJson.getArray("contents").size() - 1));
|
||||||
|
if (lastStream == null || lastStream.getObject("playlistPanelVideoRenderer") == null) {
|
||||||
|
throw new ExtractionException("Could not extract next page url");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUrlFromNavigationEndpoint(
|
||||||
|
lastStream.getObject("playlistPanelVideoRenderer").getObject("navigationEndpoint"))
|
||||||
|
+ "&pbj=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<StreamInfoItem> getPage(final Page page)
|
||||||
|
throws ExtractionException, IOException {
|
||||||
|
if (page == null || isNullOrEmpty(page.getUrl())) {
|
||||||
|
throw new IllegalArgumentException("Page url is empty or null");
|
||||||
|
}
|
||||||
|
if (!page.getCookies().containsKey(COOKIE_NAME)) {
|
||||||
|
throw new IllegalArgumentException("Cooke '" + COOKIE_NAME + "' is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonArray ajaxJson = getJsonResponse(page, getExtractorLocalization());
|
||||||
|
final JsonObject playlistJson =
|
||||||
|
ajaxJson.getObject(3).getObject("response").getObject("contents")
|
||||||
|
.getObject("twoColumnWatchNextResults").getObject("playlist")
|
||||||
|
.getObject("playlist");
|
||||||
|
final JsonArray allStreams = playlistJson.getArray("contents");
|
||||||
|
// Sublist because youtube returns up to 24 previous streams in the mix
|
||||||
|
// +1 because the stream of "currentIndex" was already extracted in previous request
|
||||||
|
final List<Object> newStreams =
|
||||||
|
allStreams.subList(playlistJson.getInt("currentIndex") + 1, allStreams.size());
|
||||||
|
|
||||||
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
collectStreamsFrom(collector, newStreams);
|
||||||
|
return new InfoItemsPage<>(collector,
|
||||||
|
new Page(getNextPageUrlFrom(playlistJson), page.getCookies()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectStreamsFrom(
|
||||||
|
@Nonnull final StreamInfoItemsCollector collector,
|
||||||
|
@Nullable final List<Object> streams) {
|
||||||
|
|
||||||
|
if (streams == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
||||||
|
|
||||||
|
for (final Object stream : streams) {
|
||||||
|
if (stream instanceof JsonObject) {
|
||||||
|
final JsonObject streamInfo = ((JsonObject) stream)
|
||||||
|
.getObject("playlistPanelVideoRenderer");
|
||||||
|
if (streamInfo != null) {
|
||||||
|
collector.commit(new YoutubeStreamInfoItemExtractor(streamInfo, timeAgoParser));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getThumbnailUrlFromPlaylistId(final String playlistId) throws ParsingException {
|
||||||
|
final String videoId;
|
||||||
|
if (playlistId.startsWith("RDMM")) {
|
||||||
|
videoId = playlistId.substring(4);
|
||||||
|
} else if (playlistId.startsWith("RDCMUC")) {
|
||||||
|
throw new ParsingException("is channel mix");
|
||||||
|
} else {
|
||||||
|
videoId = playlistId.substring(2);
|
||||||
|
}
|
||||||
|
if (videoId.isEmpty()) {
|
||||||
|
throw new ParsingException("videoId is empty");
|
||||||
|
}
|
||||||
|
return getThumbnailUrlFromVideoId(videoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getThumbnailUrlFromVideoId(final String videoId) {
|
||||||
|
return "https://i.ytimg.com/vi/" + videoId + "/hqdefault.jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelName() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelUrl() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelAvatarUrl() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -190,9 +190,10 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
||||||
return new InfoItemsPage<>(collector, null);
|
return new InfoItemsPage<>(collector, null);
|
||||||
} else if (contents.getObject(0).has("playlistVideoListRenderer")) {
|
} else if (contents.getObject(0).has("playlistVideoListRenderer")) {
|
||||||
final JsonObject videos = contents.getObject(0).getObject("playlistVideoListRenderer");
|
final JsonObject videos = contents.getObject(0).getObject("playlistVideoListRenderer");
|
||||||
collectStreamsFrom(collector, videos.getArray("contents"));
|
final JsonArray videosArray = videos.getArray("contents");
|
||||||
|
collectStreamsFrom(collector, videosArray);
|
||||||
|
|
||||||
nextPage = getNextPageFrom(videos.getArray("continuations"));
|
nextPage = getNextPageFrom(videosArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, nextPage);
|
return new InfoItemsPage<>(collector, nextPage);
|
||||||
|
@ -207,24 +208,34 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
final JsonArray ajaxJson = getJsonResponse(page.getUrl(), getExtractorLocalization());
|
final JsonArray ajaxJson = getJsonResponse(page.getUrl(), getExtractorLocalization());
|
||||||
|
|
||||||
final JsonObject sectionListContinuation = ajaxJson.getObject(1).getObject("response")
|
final JsonArray continuation = ajaxJson.getObject(1)
|
||||||
.getObject("continuationContents").getObject("playlistVideoListContinuation");
|
.getObject("response")
|
||||||
|
.getArray("onResponseReceivedActions")
|
||||||
|
.getObject(0)
|
||||||
|
.getObject("appendContinuationItemsAction")
|
||||||
|
.getArray("continuationItems");
|
||||||
|
|
||||||
collectStreamsFrom(collector, sectionListContinuation.getArray("contents"));
|
collectStreamsFrom(collector, continuation);
|
||||||
|
|
||||||
return new InfoItemsPage<>(collector, getNextPageFrom(sectionListContinuation.getArray("continuations")));
|
return new InfoItemsPage<>(collector, getNextPageFrom(continuation));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page getNextPageFrom(final JsonArray continuations) {
|
private Page getNextPageFrom(final JsonArray contents) {
|
||||||
if (isNullOrEmpty(continuations)) {
|
if (isNullOrEmpty(contents)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject nextContinuationData = continuations.getObject(0).getObject("nextContinuationData");
|
final JsonObject lastElement = contents.getObject(contents.size() - 1);
|
||||||
final String continuation = nextContinuationData.getString("continuation");
|
if (lastElement.has("continuationItemRenderer")) {
|
||||||
final String clickTrackingParams = nextContinuationData.getString("clickTrackingParams");
|
final String continuation = lastElement
|
||||||
return new Page("https://www.youtube.com/browse_ajax?ctoken=" + continuation + "&continuation=" + continuation
|
.getObject("continuationItemRenderer")
|
||||||
+ "&itct=" + clickTrackingParams);
|
.getObject("continuationEndpoint")
|
||||||
|
.getObject("continuationCommand")
|
||||||
|
.getString("token");
|
||||||
|
return new Page("https://www.youtube.com/browse_ajax?continuation=" + continuation);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectStreamsFrom(final StreamInfoItemsCollector collector, final JsonArray videos) {
|
private void collectStreamsFrom(final StreamInfoItemsCollector collector, final JsonArray videos) {
|
||||||
|
|
|
@ -55,6 +55,7 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
||||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
|
||||||
|
@ -861,7 +862,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
||||||
} finally {
|
} finally {
|
||||||
Context.exit();
|
Context.exit();
|
||||||
}
|
}
|
||||||
return result == null ? "" : result.toString();
|
return Objects.toString(result, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -1,60 +1,72 @@
|
||||||
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
|
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
|
||||||
|
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class YoutubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
public class YoutubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||||
|
|
||||||
private static final YoutubePlaylistLinkHandlerFactory instance = new YoutubePlaylistLinkHandlerFactory();
|
private static final YoutubePlaylistLinkHandlerFactory INSTANCE =
|
||||||
|
new YoutubePlaylistLinkHandlerFactory();
|
||||||
|
|
||||||
public static YoutubePlaylistLinkHandlerFactory getInstance() {
|
public static YoutubePlaylistLinkHandlerFactory getInstance() {
|
||||||
return instance;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl(String id, List<String> contentFilters, String sortFilter) {
|
public String getUrl(final String id, final List<String> contentFilters,
|
||||||
|
final String sortFilter) {
|
||||||
return "https://www.youtube.com/playlist?list=" + id;
|
return "https://www.youtube.com/playlist?list=" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId(String url) throws ParsingException {
|
public String getId(final String url) throws ParsingException {
|
||||||
try {
|
try {
|
||||||
URL urlObj = Utils.stringToURL(url);
|
final URL urlObj = Utils.stringToURL(url);
|
||||||
|
|
||||||
if (!Utils.isHTTP(urlObj) || !(YoutubeParsingHelper.isYoutubeURL(urlObj)
|
if (!Utils.isHTTP(urlObj) || !(YoutubeParsingHelper.isYoutubeURL(urlObj)
|
||||||
|| YoutubeParsingHelper.isInvidioURL(urlObj))) {
|
|| YoutubeParsingHelper.isInvidioURL(urlObj))) {
|
||||||
throw new ParsingException("the url given is not a Youtube-URL");
|
throw new ParsingException("the url given is not a Youtube-URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = urlObj.getPath();
|
final String path = urlObj.getPath();
|
||||||
if (!path.equals("/watch") && !path.equals("/playlist")) {
|
if (!path.equals("/watch") && !path.equals("/playlist")) {
|
||||||
throw new ParsingException("the url given is neither a video nor a playlist URL");
|
throw new ParsingException("the url given is neither a video nor a playlist URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
String listID = Utils.getQueryValue(urlObj, "list");
|
final String listID = Utils.getQueryValue(urlObj, "list");
|
||||||
|
|
||||||
if (listID == null) {
|
if (listID == null) {
|
||||||
throw new ParsingException("the url given does not include a playlist");
|
throw new ParsingException("the url given does not include a playlist");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!listID.matches("[a-zA-Z0-9_-]{10,}")) {
|
if (!listID.matches("[a-zA-Z0-9_-]{10,}")) {
|
||||||
throw new ParsingException("the list-ID given in the URL does not match the list pattern");
|
throw new ParsingException(
|
||||||
|
"the list-ID given in the URL does not match the list pattern");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't accept auto-generated "Mix" playlists but auto-generated YouTube Music playlists
|
if (YoutubeParsingHelper.isYoutubeMusicMixId(listID)) {
|
||||||
if (listID.startsWith("RD") && !listID.startsWith("RDCLAK")) {
|
throw new ContentNotSupportedException(
|
||||||
throw new ContentNotSupportedException("YouTube Mix playlists are not yet supported");
|
"YouTube Music Mix playlists are not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (YoutubeParsingHelper.isYoutubeChannelMixId(listID)
|
||||||
|
&& Utils.getQueryValue(urlObj, "v") == null) {
|
||||||
|
//Video id can't be determined from the channel mix id. See YoutubeParsingHelper#extractVideoIdFromMixId
|
||||||
|
throw new ContentNotSupportedException("Channel Mix without a video id are not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
return listID;
|
return listID;
|
||||||
} catch (final Exception exception) {
|
} catch (final Exception exception) {
|
||||||
throw new ParsingException("Error could not parse url :" + exception.getMessage(), exception);
|
throw new ParsingException("Error could not parse url :" + exception.getMessage(),
|
||||||
|
exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,4 +79,33 @@ public class YoutubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * If it is a mix (auto-generated playlist) URL, return a {@link LinkHandler} where the URL is
|
||||||
|
* like
|
||||||
|
* <code>https://youtube.com/watch?v=videoId&list=playlistId</code>.
|
||||||
|
* <p>Otherwise use super</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ListLinkHandler fromUrl(final String url) throws ParsingException {
|
||||||
|
try {
|
||||||
|
final URL urlObj = Utils.stringToURL(url);
|
||||||
|
final String listID = Utils.getQueryValue(urlObj, "list");
|
||||||
|
if (listID != null && YoutubeParsingHelper.isYoutubeMixId(listID)) {
|
||||||
|
String videoID = Utils.getQueryValue(urlObj, "v");
|
||||||
|
if (videoID == null) {
|
||||||
|
videoID = YoutubeParsingHelper.extractVideoIdFromMixId(listID);
|
||||||
|
}
|
||||||
|
final String newUrl = "https://www.youtube.com/watch?v=" + videoID
|
||||||
|
+ "&list=" + listID;
|
||||||
|
return new ListLinkHandler(new LinkHandler(url, newUrl, listID),
|
||||||
|
getContentFilter(url),
|
||||||
|
getSortFilter(url));
|
||||||
|
}
|
||||||
|
} catch (MalformedURLException exception) {
|
||||||
|
throw new ParsingException("Error could not parse url :" + exception.getMessage(),
|
||||||
|
exception);
|
||||||
|
}
|
||||||
|
return super.fromUrl(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,20 +186,18 @@ public class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
||||||
case "WWW.INVIDIO.US":
|
case "WWW.INVIDIO.US":
|
||||||
case "DEV.INVIDIO.US":
|
case "DEV.INVIDIO.US":
|
||||||
case "INVIDIO.US":
|
case "INVIDIO.US":
|
||||||
|
case "VID.ENCRYPTIONIN.SPACE":
|
||||||
case "INVIDIOUS.SNOPYTA.ORG":
|
case "INVIDIOUS.SNOPYTA.ORG":
|
||||||
case "FI.INVIDIOUS.SNOPYTA.ORG":
|
|
||||||
case "YEWTU.BE":
|
case "YEWTU.BE":
|
||||||
case "INVIDIOUS.GGC-PROJECT.DE":
|
case "TUBE.CONNECT.CAFE":
|
||||||
case "YT.MAISPUTAIN.OVH":
|
case "INVIDIOUS.ZAPASHCANON.FR":
|
||||||
case "INVIDIOUS.13AD.DE":
|
case "INVIDIOUS.KAVIN.ROCKS":
|
||||||
case "INVIDIOUS.TOOT.KOELN":
|
case "INVIDIOUS.TUBE":
|
||||||
case "INVIDIOUS.FDN.FR":
|
case "INVIDIOUS.SITE":
|
||||||
case "WATCH.NETTOHIKARI.COM":
|
case "INVIDIOUS.XYZ":
|
||||||
case "INVIDIOUS.SNWMDS.NET":
|
case "VID.MINT.LGBT":
|
||||||
case "INVIDIOUS.SNWMDS.ORG":
|
case "INVIDIOU.SITE":
|
||||||
case "INVIDIOUS.SNWMDS.COM":
|
case "INVIDIOUS.FDN.FR": { // code-block for hooktube.com and Invidious instances
|
||||||
case "INVIDIOUS.SUNSETRAVENS.COM":
|
|
||||||
case "INVIDIOUS.GACHIRANGERS.COM": { // code-block for hooktube.com and Invidious instances
|
|
||||||
if (path.equals("watch")) {
|
if (path.equals("watch")) {
|
||||||
String viewQueryValue = Utils.getQueryValue(url, "v");
|
String viewQueryValue = Utils.getQueryValue(url, "v");
|
||||||
if (viewQueryValue != null) {
|
if (viewQueryValue != null) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import java.net.URL;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -260,4 +261,15 @@ public class Utils {
|
||||||
}
|
}
|
||||||
return stringBuilder.toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String join(final String delimiter, final String mapJoin,
|
||||||
|
final Map<? extends CharSequence, ? extends CharSequence> elements) {
|
||||||
|
final List<String> list = new LinkedList<>();
|
||||||
|
for (final Map.Entry<? extends CharSequence, ? extends CharSequence> entry : elements
|
||||||
|
.entrySet()) {
|
||||||
|
list.add(entry.getKey() + mapJoin + entry.getValue());
|
||||||
|
}
|
||||||
|
return join(delimiter, list);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class YoutubeCommentsExtractorTest {
|
||||||
private static final String url = "https://www.youtube.com/watch?v=D00Au7k3i6o";
|
private static final String url = "https://www.youtube.com/watch?v=D00Au7k3i6o";
|
||||||
private static YoutubeCommentsExtractor extractor;
|
private static YoutubeCommentsExtractor extractor;
|
||||||
|
|
||||||
private static final String commentContent = "sub 4 sub";
|
private static final String commentContent = "Category: Education";
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
|
|
|
@ -0,0 +1,345 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.youtube;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.hamcrest.MatcherAssert;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Suite;
|
||||||
|
import org.junit.runners.Suite.SuiteClasses;
|
||||||
|
import org.schabi.newpipe.DownloaderTestImpl;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
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.stream.StreamInfoItem;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.hamcrest.CoreMatchers.startsWith;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
|
|
||||||
|
@RunWith(Suite.class)
|
||||||
|
@SuiteClasses({Mix.class, MixWithIndex.class, MyMix.class, Invalid.class, ChannelMix.class})
|
||||||
|
public class YoutubeMixPlaylistExtractorTest {
|
||||||
|
|
||||||
|
public static final String PBJ = "&pbj=1";
|
||||||
|
private static final String VIDEO_ID = "_AzeUSL9lZc";
|
||||||
|
private static final String VIDEO_TITLE =
|
||||||
|
"Most Beautiful And Emotional Piano: Anime Music Shigatsu wa Kimi no Uso OST IMO";
|
||||||
|
private static final Map<String, String> dummyCookie
|
||||||
|
= Collections.singletonMap(YoutubeMixPlaylistExtractor.COOKIE_NAME, "whatever");
|
||||||
|
|
||||||
|
private static YoutubeMixPlaylistExtractor extractor;
|
||||||
|
|
||||||
|
public static class Mix {
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RD" + VIDEO_ID);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getServiceId() {
|
||||||
|
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getName() throws Exception {
|
||||||
|
final String name = extractor.getName();
|
||||||
|
assertThat(name, startsWith("Mix"));
|
||||||
|
assertThat(name, containsString(VIDEO_TITLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getThumbnailUrl() throws Exception {
|
||||||
|
final String thumbnailUrl = extractor.getThumbnailUrl();
|
||||||
|
assertIsSecureUrl(thumbnailUrl);
|
||||||
|
MatcherAssert.assertThat(thumbnailUrl, containsString("yt"));
|
||||||
|
assertThat(thumbnailUrl, containsString(VIDEO_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInitialPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(
|
||||||
|
new Page("https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RD" + VIDEO_ID
|
||||||
|
+ PBJ, dummyCookie));
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getContinuations() throws Exception {
|
||||||
|
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
final Set<String> urls = new HashSet<>();
|
||||||
|
|
||||||
|
//Should work infinitely, but for testing purposes only 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
|
||||||
|
for (final StreamInfoItem item : streams.getItems()) {
|
||||||
|
assertFalse(urls.contains(item.getUrl()));
|
||||||
|
urls.add(item.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
streams = extractor.getPage(streams.getNextPage());
|
||||||
|
}
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getStreamCount() {
|
||||||
|
assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MixWithIndex {
|
||||||
|
|
||||||
|
private static final String INDEX = "&index=13";
|
||||||
|
private static final String VIDEO_ID_NUMBER_13 = "qHtzO49SDmk";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + VIDEO_ID_NUMBER_13 + "&list=RD"
|
||||||
|
+ VIDEO_ID + INDEX);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getName() throws Exception {
|
||||||
|
final String name = extractor.getName();
|
||||||
|
assertThat(name, startsWith("Mix"));
|
||||||
|
assertThat(name, containsString(VIDEO_TITLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getThumbnailUrl() throws Exception {
|
||||||
|
final String thumbnailUrl = extractor.getThumbnailUrl();
|
||||||
|
assertIsSecureUrl(thumbnailUrl);
|
||||||
|
assertThat(thumbnailUrl, containsString("yt"));
|
||||||
|
assertThat(thumbnailUrl, containsString(VIDEO_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInitialPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(
|
||||||
|
new Page("https://www.youtube.com/watch?v=" + VIDEO_ID_NUMBER_13 + "&list=RD"
|
||||||
|
+ VIDEO_ID + INDEX + PBJ, dummyCookie));
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getContinuations() throws Exception {
|
||||||
|
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
final Set<String> urls = new HashSet<>();
|
||||||
|
|
||||||
|
//Should work infinitely, but for testing purposes only 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
for (final StreamInfoItem item : streams.getItems()) {
|
||||||
|
assertFalse(urls.contains(item.getUrl()));
|
||||||
|
urls.add(item.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
streams = extractor.getPage(streams.getNextPage());
|
||||||
|
}
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getStreamCount() {
|
||||||
|
assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MyMix {
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RDMM"
|
||||||
|
+ VIDEO_ID);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getServiceId() {
|
||||||
|
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getName() throws Exception {
|
||||||
|
final String name = extractor.getName();
|
||||||
|
assertEquals("My Mix", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getThumbnailUrl() throws Exception {
|
||||||
|
final String thumbnailUrl = extractor.getThumbnailUrl();
|
||||||
|
assertIsSecureUrl(thumbnailUrl);
|
||||||
|
assertThat(thumbnailUrl, startsWith("https://i.ytimg.com/vi/_AzeUSL9lZc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInitialPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams =
|
||||||
|
extractor.getPage(new Page("https://www.youtube.com/watch?v=" + VIDEO_ID
|
||||||
|
+ "&list=RDMM" + VIDEO_ID + PBJ, dummyCookie));
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void getContinuations() throws Exception {
|
||||||
|
InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
final Set<String> urls = new HashSet<>();
|
||||||
|
|
||||||
|
//Should work infinitely, but for testing purposes only 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
|
||||||
|
for (final StreamInfoItem item : streams.getItems()) {
|
||||||
|
assertFalse(urls.contains(item.getUrl()));
|
||||||
|
urls.add(item.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
streams = extractor.getPage(streams.getNextPage());
|
||||||
|
}
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getStreamCount() {
|
||||||
|
assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Invalid {
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getPageEmptyUrl() throws Exception {
|
||||||
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + VIDEO_ID + "&list=RD" + VIDEO_ID);
|
||||||
|
extractor.fetchPage();
|
||||||
|
extractor.getPage(new Page(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = ExtractionException.class)
|
||||||
|
public void invalidVideoId() throws Exception {
|
||||||
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + "abcde" + "&list=RD" + "abcde");
|
||||||
|
extractor.fetchPage();
|
||||||
|
extractor.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ChannelMix {
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "UCXuqSBlHAE6Xw-yeJA0Tunw";
|
||||||
|
private static final String VIDEO_ID_OF_CHANNEL = "mnk6gnOBYIo";
|
||||||
|
private static final String CHANNEL_TITLE = "Linus Tech Tips";
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = (YoutubeMixPlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
|
||||||
|
+ "&list=RDCM" + CHANNEL_ID);
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getName() throws Exception {
|
||||||
|
final String name = extractor.getName();
|
||||||
|
assertThat(name, startsWith("Mix"));
|
||||||
|
assertThat(name, containsString(CHANNEL_TITLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getThumbnailUrl() throws Exception {
|
||||||
|
final String thumbnailUrl = extractor.getThumbnailUrl();
|
||||||
|
assertIsSecureUrl(thumbnailUrl);
|
||||||
|
assertThat(thumbnailUrl, containsString("yt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInitialPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getInitialPage();
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getPage() throws Exception {
|
||||||
|
final InfoItemsPage<StreamInfoItem> streams = extractor.getPage(
|
||||||
|
new Page("https://www.youtube.com/watch?v=" + VIDEO_ID_OF_CHANNEL
|
||||||
|
+ "&list=RDCM" + CHANNEL_ID + PBJ, dummyCookie));
|
||||||
|
assertFalse(streams.getItems().isEmpty());
|
||||||
|
assertTrue(streams.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getStreamCount() {
|
||||||
|
assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,9 @@ 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.DownloaderTestImpl;
|
import org.schabi.newpipe.DownloaderTestImpl;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
@ -11,10 +14,17 @@ 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;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertFalse;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
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;
|
||||||
|
@ -23,6 +33,9 @@ import static org.schabi.newpipe.extractor.services.DefaultTests.*;
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
|
|
||||||
public static class NotAvailable {
|
public static class NotAvailable {
|
||||||
|
@ -114,7 +127,7 @@ public class YoutubePlaylistExtractorTest {
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
public void testBannerUrl() throws Exception {
|
public void testBannerUrl() {
|
||||||
final String bannerUrl = extractor.getBannerUrl();
|
final String bannerUrl = extractor.getBannerUrl();
|
||||||
assertIsSecureUrl(bannerUrl);
|
assertIsSecureUrl(bannerUrl);
|
||||||
assertTrue(bannerUrl, bannerUrl.contains("yt"));
|
assertTrue(bannerUrl, bannerUrl.contains("yt"));
|
||||||
|
@ -227,7 +240,7 @@ public class YoutubePlaylistExtractorTest {
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
public void testBannerUrl() throws Exception {
|
public void testBannerUrl() {
|
||||||
final String bannerUrl = extractor.getBannerUrl();
|
final String bannerUrl = extractor.getBannerUrl();
|
||||||
assertIsSecureUrl(bannerUrl);
|
assertIsSecureUrl(bannerUrl);
|
||||||
assertTrue(bannerUrl, bannerUrl.contains("yt"));
|
assertTrue(bannerUrl, bannerUrl.contains("yt"));
|
||||||
|
@ -324,7 +337,7 @@ public class YoutubePlaylistExtractorTest {
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
public void testBannerUrl() throws Exception {
|
public void testBannerUrl() {
|
||||||
final String bannerUrl = extractor.getBannerUrl();
|
final String bannerUrl = extractor.getBannerUrl();
|
||||||
assertIsSecureUrl(bannerUrl);
|
assertIsSecureUrl(bannerUrl);
|
||||||
assertTrue(bannerUrl, bannerUrl.contains("yt"));
|
assertTrue(bannerUrl, bannerUrl.contains("yt"));
|
||||||
|
@ -352,4 +365,34 @@ public class YoutubePlaylistExtractorTest {
|
||||||
assertTrue("Error in the streams count", extractor.getStreamCount() > 40);
|
assertTrue("Error in the streams count", extractor.getStreamCount() > 40);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ContinuationsTests {
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoContinuations() throws Exception {
|
||||||
|
final YoutubePlaylistExtractor extractor = (YoutubePlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/playlist?list=PLXJg25X-OulsVsnvZ7RVtSDW-id9_RzAO");
|
||||||
|
extractor.fetchPage();
|
||||||
|
|
||||||
|
assertNoMoreItems(extractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnlySingleContinuation() throws Exception {
|
||||||
|
final YoutubePlaylistExtractor extractor = (YoutubePlaylistExtractor) YouTube
|
||||||
|
.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/playlist?list=PLjgwFL8urN2DFRuRkFTkmtHjyoNWHHdZX");
|
||||||
|
extractor.fetchPage();
|
||||||
|
|
||||||
|
final ListExtractor.InfoItemsPage<StreamInfoItem> page = defaultTestMoreItems(
|
||||||
|
extractor);
|
||||||
|
assertFalse("More items available when it shouldn't", page.hasNextPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ public class YoutubePlaylistLinkHandlerFactoryTest {
|
||||||
assertTrue(linkHandler.acceptUrl("www.youtube.com/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
|
assertTrue(linkHandler.acceptUrl("www.youtube.com/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV"));
|
||||||
assertTrue(linkHandler.acceptUrl("https://music.youtube.com/playlist?list=OLAK5uy_lEBUW9iTwqf0IlYPxZ8LrzpgqjAHZgZpM"));
|
assertTrue(linkHandler.acceptUrl("https://music.youtube.com/playlist?list=OLAK5uy_lEBUW9iTwqf0IlYPxZ8LrzpgqjAHZgZpM"));
|
||||||
assertTrue(linkHandler.acceptUrl("https://www.youtube.com/playlist?list=RDCLAK5uy_ly6s4irLuZAcjEDwJmqcA_UtSipMyGgbQ")); // YouTube Music playlist
|
assertTrue(linkHandler.acceptUrl("https://www.youtube.com/playlist?list=RDCLAK5uy_ly6s4irLuZAcjEDwJmqcA_UtSipMyGgbQ")); // YouTube Music playlist
|
||||||
assertFalse(linkHandler.acceptUrl("https://www.youtube.com/watch?v=2kZVEUGLgy4&list=RDdoEcQv1wlsI&index=2, ")); // YouTube Mix
|
assertTrue(linkHandler.acceptUrl("https://www.youtube.com/watch?v=2kZVEUGLgy4&list=RDdoEcQv1wlsI&index=2, ")); // YouTube Mix
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -105,4 +105,23 @@ public class YoutubePlaylistLinkHandlerFactoryTest {
|
||||||
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
|
assertEquals("PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC", linkHandler.fromUrl("www.invidio.us/playlist?list=PLW5y1tjAOzI3orQNF1yGGVL5x-pR2K1dC").getId());
|
||||||
assertEquals("PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV", linkHandler.fromUrl("www.invidio.us/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV").getId());
|
assertEquals("PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV", linkHandler.fromUrl("www.invidio.us/playlist?list=PLz8YL4HVC87WJQDzVoY943URKQCsHS9XV").getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromUrlIsMixVideo() throws Exception {
|
||||||
|
final String videoId = "_AzeUSL9lZc";
|
||||||
|
String url = "https://www.youtube.com/watch?v=" + videoId + "&list=RD" + videoId;
|
||||||
|
assertEquals(url, linkHandler.fromUrl(url).getUrl());
|
||||||
|
|
||||||
|
final String mixVideoId = "qHtzO49SDmk";
|
||||||
|
url = "https://www.youtube.com/watch?v=" + mixVideoId + "&list=RD" + videoId;
|
||||||
|
assertEquals(url, linkHandler.fromUrl(url).getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromUrlIsMixPlaylist() throws Exception {
|
||||||
|
final String videoId = "_AzeUSL9lZc";
|
||||||
|
final String url = "https://www.youtube.com/watch?v=" + videoId + "&list=RD" + videoId;
|
||||||
|
assertEquals(url,
|
||||||
|
linkHandler.fromUrl("https://www.youtube.com/watch?list=RD" + videoId).getUrl());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -26,9 +26,13 @@ import org.schabi.newpipe.DownloaderTestImpl;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMixPlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubePlaylistExtractor;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
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.schabi.newpipe.extractor.ServiceList.YouTube;
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,4 +58,30 @@ public class YoutubeServiceTest {
|
||||||
public void testGetDefaultKiosk() throws Exception {
|
public void testGetDefaultKiosk() throws Exception {
|
||||||
assertEquals(kioskList.getDefaultKioskExtractor(null).getId(), "Trending");
|
assertEquals(kioskList.getDefaultKioskExtractor(null).getId(), "Trending");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getPlayListExtractorIsNormalPlaylist() throws Exception {
|
||||||
|
final PlaylistExtractor extractor = service.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=JhqtYOnNrTs&list=PL-EkZZikQIQVqk9rBWzEo5b-2GeozElS");
|
||||||
|
assertTrue(extractor instanceof YoutubePlaylistExtractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getPlaylistExtractorIsMix() throws Exception {
|
||||||
|
final String videoId = "_AzeUSL9lZc";
|
||||||
|
PlaylistExtractor extractor = YouTube.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + videoId + "&list=RD" + videoId);
|
||||||
|
assertTrue(extractor instanceof YoutubeMixPlaylistExtractor);
|
||||||
|
|
||||||
|
extractor = YouTube.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + videoId + "&list=RDMM" + videoId);
|
||||||
|
assertTrue(extractor instanceof YoutubeMixPlaylistExtractor);
|
||||||
|
|
||||||
|
final String mixVideoId = "qHtzO49SDmk";
|
||||||
|
|
||||||
|
extractor = YouTube.getPlaylistExtractor(
|
||||||
|
"https://www.youtube.com/watch?v=" + mixVideoId + "&list=RD" + videoId);
|
||||||
|
assertTrue(extractor instanceof YoutubeMixPlaylistExtractor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue