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.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; 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 javax.annotation.Nonnull; import javax.annotation.Nullable; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint; /** * A YoutubePlaylistExtractor for a mix (auto-generated playlist). It handles urls in the format of * "youtube.com/watch?v=videoId&list=playlistId" */ public class YoutubeMixPlaylistExtractor extends PlaylistExtractor { private JsonObject initialData; private JsonObject playlistData; public YoutubeMixPlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) { super(service, linkHandler); } @Override public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException { final String url = getUrl() + "&pbj=1"; final JsonArray ajaxJson = getJsonResponse(url, getExtractorLocalization()); initialData = ajaxJson.getObject(3).getObject("response"); playlistData = initialData.getObject("contents").getObject("twoColumnWatchNextResults") .getObject("playlist").getObject("playlist"); } @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 { final String playlistId = playlistData.getString("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); } } @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 getInitialPage() throws ExtractionException { StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); collectStreamsFrom(collector, playlistData.getArray("contents")); return new InfoItemsPage<>(collector, getNextPageUrl()); } @Override public String getNextPageUrl() throws ExtractionException { 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"); } //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 public InfoItemsPage getPage(final String pageUrl) throws ExtractionException, IOException { if (pageUrl == null || pageUrl.isEmpty()) { throw new ExtractionException( new IllegalArgumentException("Page url is empty or null")); } StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); final JsonArray ajaxJson = getJsonResponse(pageUrl, getExtractorLocalization()); 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, getNextPageUrlFrom(playlistData)); } private void collectStreamsFrom( @Nonnull StreamInfoItemsCollector collector, @Nullable JsonArray streams) { if (streams == null) { return; } final TimeAgoParser timeAgoParser = getTimeAgoParser(); for (Object stream : streams) { if (stream instanceof JsonObject) { JsonObject streamInfo = ((JsonObject) stream) .getObject("playlistPanelVideoRenderer"); if (streamInfo != null) { collector.commit(new YoutubeStreamInfoItemExtractor(streamInfo, timeAgoParser)); } } } } 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"; } @Nonnull @Override public String getSubChannelName() { return ""; } @Nonnull @Override public String getSubChannelUrl() { return ""; } @Nonnull @Override public String getSubChannelAvatarUrl() { return ""; } }