diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 258799692..4ac9724b2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -193,13 +193,23 @@ public class YoutubeParsingHelper { } /** - * Checks if the given playlist id is a mix (auto-generated playlist) - * Ids from a mix start with "RD" + * 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 mix + * @return Whether given id belongs to a youtube mix */ public static boolean isYoutubeMixId(String playlistId) { - return playlistId.startsWith("RD"); + 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 "RD" + * @param playlistId + * @return Whether given id belongs to a youtube music mix + */ + public static boolean isYoutubeMusicMixId(String playlistId) { + return playlistId.startsWith("RDAMVM"); } public static JsonObject getInitialData(String html) throws ParsingException { @@ -427,9 +437,9 @@ public class YoutubeParsingHelper { StringBuilder url = new StringBuilder(); url.append("https://www.youtube.com/watch?v=").append(navigationEndpoint.getObject("watchEndpoint").getString("videoId")); if (navigationEndpoint.getObject("watchEndpoint").has("playlistId")) - url.append("&list=").append(navigationEndpoint.getObject("watchEndpoint").getString("playlistId")); + url.append("&list=").append(navigationEndpoint.getObject("watchEndpoint").getString("playlistId")); if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds")) - url.append("&t=").append(navigationEndpoint.getObject("watchEndpoint").getInt("startTimeSeconds")); + url.append("&t=").append(navigationEndpoint.getObject("watchEndpoint").getInt("startTimeSeconds")); return url.toString(); } else if (navigationEndpoint.has("watchPlaylistEndpoint")) { return "https://www.youtube.com/playlist?list=" + @@ -457,6 +467,7 @@ public class YoutubeParsingHelper { if (html && ((JsonObject) textPart).has("navigationEndpoint")) { String url = getUrlFromNavigationEndpoint(((JsonObject) textPart).getObject("navigationEndpoint")); if (!isNullOrEmpty(url)) { + url = url.replaceAll("&", "&"); textBuilder.append("").append(text).append(""); continue; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java index 7d7a83eba..fce5a1d46 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java @@ -144,7 +144,7 @@ public class YoutubeService extends StreamingService { public KioskExtractor createNewKiosk(StreamingService streamingService, String url, String id) - throws ExtractionException { + throws ExtractionException { return new YoutubeTrendingExtractor(YoutubeService.this, new YoutubeTrendingLinkHandlerFactory().fromUrl(url), id); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMixPlaylistExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMixPlaylistExtractor.java index 060edc20b..0aedafd0e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMixPlaylistExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeMixPlaylistExtractor.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.extractor.services.youtube.extractors; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse; +import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getUrlFromNavigationEndpoint; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -25,13 +26,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; */ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { - - private final static String CONTENTS = "contents"; - private final static String RESPONSE = "response"; - private final static String PLAYLIST = "playlist"; - private final static String TWO_COLUMN_WATCH_NEXT_RESULTS = "twoColumnWatchNextResults"; - private final static String PLAYLIST_PANEL_VIDEO_RENDERER = "playlistPanelVideoRenderer"; - + private JsonObject initialData; private JsonObject playlistData; public YoutubeMixPlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) { @@ -43,9 +38,9 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { throws IOException, ExtractionException { final String url = getUrl() + "&pbj=1"; final JsonArray ajaxJson = getJsonResponse(url, getExtractorLocalization()); - JsonObject initialData = ajaxJson.getObject(3).getObject(RESPONSE); - playlistData = initialData.getObject(CONTENTS).getObject(TWO_COLUMN_WATCH_NEXT_RESULTS) - .getObject(PLAYLIST).getObject(PLAYLIST); + initialData = ajaxJson.getObject(3).getObject("response"); + playlistData = initialData.getObject("contents").getObject("twoColumnWatchNextResults") + .getObject("playlist").getObject("playlist"); } @Nonnull @@ -62,7 +57,14 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { public String getThumbnailUrl() throws ParsingException { try { final String playlistId = playlistData.getString("playlistId"); - return getThumbnailUrlFromId(playlistId); + try { + return getThumbnailUrlFromPlaylistId(playlistId); + } catch (ParsingException e) { + //fallback to thumbnail of current video. Always the case for channel mix + return getThumbnailUrlFromVideoId( + initialData.getObject("currentVideoEndpoint").getObject("watchEndpoint") + .getString("videoId")); + } } catch (Exception e) { throw new ParsingException("Could not get playlist thumbnail", e); } @@ -101,20 +103,26 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { @Override public InfoItemsPage getInitialPage() throws ExtractionException { StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); - collectStreamsFrom(collector, playlistData.getArray(CONTENTS)); + collectStreamsFrom(collector, playlistData.getArray("contents")); return new InfoItemsPage<>(collector, getNextPageUrl()); } @Override public String getNextPageUrl() throws ExtractionException { - final JsonObject lastStream = ((JsonObject) playlistData.getArray(CONTENTS) - .get(playlistData.getArray(CONTENTS).size() - 1)); - if (lastStream == null || lastStream.getObject(PLAYLIST_PANEL_VIDEO_RENDERER) == null) { + return getNextPageUrlFrom(playlistData); + } + + private String getNextPageUrlFrom(JsonObject playlistData) throws ExtractionException { + final JsonObject lastStream = ((JsonObject) playlistData.getArray("contents") + .get(playlistData.getArray("contents").size() - 1)); + if (lastStream == null || lastStream.getObject("playlistPanelVideoRenderer") == null) { throw new ExtractionException("Could not extract next page url"); } - return "https://youtube.com" + lastStream.getObject(PLAYLIST_PANEL_VIDEO_RENDERER) - .getObject("navigationEndpoint").getObject("commandMetadata") - .getObject("webCommandMetadata").getString("url") + "&pbj=1"; + //Index of video in mix is missing, but adding it doesn't appear to have any effect. + //And since the index needs to be tracked by us, it is left out + return getUrlFromNavigationEndpoint( + lastStream.getObject("playlistPanelVideoRenderer").getObject("navigationEndpoint")) + + "&pbj=1"; } @Override @@ -127,21 +135,20 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); final JsonArray ajaxJson = getJsonResponse(pageUrl, getExtractorLocalization()); - playlistData = - ajaxJson.getObject(3).getObject(RESPONSE).getObject(CONTENTS) - .getObject(TWO_COLUMN_WATCH_NEXT_RESULTS).getObject(PLAYLIST) - .getObject(PLAYLIST); - final JsonArray streams = playlistData.getArray(CONTENTS); + JsonObject playlistData = + ajaxJson.getObject(3).getObject("response").getObject("contents") + .getObject("twoColumnWatchNextResults").getObject("playlist") + .getObject("playlist"); + final JsonArray streams = playlistData.getArray("contents"); //Because continuation requests are created with the last video of previous request as start streams.remove(0); collectStreamsFrom(collector, streams); - return new InfoItemsPage<>(collector, getNextPageUrl()); + return new InfoItemsPage<>(collector, getNextPageUrlFrom(playlistData)); } private void collectStreamsFrom( @Nonnull StreamInfoItemsCollector collector, @Nullable JsonArray streams) { - collector.reset(); if (streams == null) { return; @@ -152,7 +159,7 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { for (Object stream : streams) { if (stream instanceof JsonObject) { JsonObject streamInfo = ((JsonObject) stream) - .getObject(PLAYLIST_PANEL_VIDEO_RENDERER); + .getObject("playlistPanelVideoRenderer"); if (streamInfo != null) { collector.commit(new YoutubeStreamInfoItemExtractor(streamInfo, timeAgoParser)); } @@ -160,16 +167,22 @@ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { } } - private String getThumbnailUrlFromId(String playlistId) throws ParsingException { + private String getThumbnailUrlFromPlaylistId(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(String videoId) { return "https://i.ytimg.com/vi/" + videoId + "/hqdefault.jpg"; } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java index 787372dcb..b565fde62 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/linkHandler/YoutubePlaylistLinkHandlerFactory.java @@ -64,11 +64,12 @@ public class YoutubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory { @Override public boolean onAcceptUrl(final String url) { try { - getId(url); + String playlistId = getId(url); + //Because youtube music mix are not supported yet. + return !YoutubeParsingHelper.isYoutubeMusicMixId(playlistId); } catch (ParsingException e) { return false; } - return true; } /** diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeMixPlaylistExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeMixPlaylistExtractorTest.java index d18e4fc55..b612827a7 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeMixPlaylistExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeMixPlaylistExtractorTest.java @@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMixPlaylistExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -93,11 +94,6 @@ public class YoutubeMixPlaylistExtractorTest { public void getStreamCount() throws Exception { assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount()); } - - @Test - public void getStreamCount() throws Exception { - assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount()); - } } public static class MixWithIndex { @@ -166,11 +162,6 @@ public class YoutubeMixPlaylistExtractorTest { assertFalse(streams.getItems().isEmpty()); } - @Test - public void getStreamCount() { - assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount()); - } - @Test public void getStreamCount() throws Exception { assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount()); @@ -264,12 +255,13 @@ public class YoutubeMixPlaylistExtractorTest { extractor.getPage(""); } - @Test(expected = NullPointerException.class) + @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(); } } @@ -329,10 +321,5 @@ public class YoutubeMixPlaylistExtractorTest { public void getStreamCount() throws Exception { assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount()); } - - @Test - public void getStreamCount() throws Exception { - assertEquals(ListExtractor.ITEM_COUNT_INFINITE, extractor.getStreamCount()); - } } } \ No newline at end of file