package org.schabi.newpipe.extractor.services.youtube; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.schabi.newpipe.extractor.Downloader; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.playlist.PlaylistExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Utils; import javax.annotation.Nonnull; import java.io.IOException; @SuppressWarnings("WeakerAccess") public class YoutubePlaylistExtractor extends PlaylistExtractor { private Document doc; /** * It's lazily initialized (when getInfoItemPage is called) */ private Document nextStreamsAjax; public YoutubePlaylistExtractor(StreamingService service, String url, String nextPageUrl) throws IOException, ExtractionException { super(service, url, nextPageUrl); } @Override public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException { String pageContent = downloader.download(getCleanUrl()); doc = Jsoup.parse(pageContent, getCleanUrl()); nextPageUrl = getNextPageUrlFrom(doc); nextStreamsAjax = null; } @Nonnull @Override public String getId() throws ParsingException { try { return getUrlIdHandler().getId(getCleanUrl()); } catch (Exception e) { throw new ParsingException("Could not get playlist id"); } } @Nonnull @Override public String getName() throws ParsingException { try { return doc.select("div[id=pl-header] h1[class=pl-header-title]").first().text(); } catch (Exception e) { throw new ParsingException("Could not get playlist name"); } } @Override public String getThumbnailUrl() throws ParsingException { try { return doc.select("div[id=pl-header] div[class=pl-header-thumb] img").first().attr("abs:src"); } catch (Exception e) { throw new ParsingException("Could not get playlist thumbnail"); } } @Override public String getBannerUrl() throws ParsingException { try { Element el = doc.select("div[id=\"gh-banner\"] style").first(); String cssContent = el.html(); String url = "https:" + Parser.matchGroup1("url\\((.*)\\)", cssContent); if (url.contains("s.ytimg.com")) { return null; } else { return url.substring(0, url.indexOf(");")); } } catch (Exception e) { throw new ParsingException("Could not get playlist Banner"); } } @Override public String getUploaderUrl() throws ParsingException { try { return doc.select("ul[class=\"pl-header-details\"] li").first().select("a").first().attr("abs:href"); } catch (Exception e) { throw new ParsingException("Could not get playlist uploader name"); } } @Override public String getUploaderName() throws ParsingException { try { return doc.select("span[class=\"qualified-channel-title-text\"]").first().select("a").first().text(); } catch (Exception e) { throw new ParsingException("Could not get playlist uploader name"); } } @Override public String getUploaderAvatarUrl() throws ParsingException { try { return doc.select("div[id=gh-banner] img[class=channel-header-profile-image]").first().attr("abs:src"); } catch (Exception e) { throw new ParsingException("Could not get playlist uploader avatar"); } } @Override public long getStreamCount() throws ParsingException { String input; try { input = doc.select("ul[class=\"pl-header-details\"] li").get(1).text(); } catch (IndexOutOfBoundsException e) { throw new ParsingException("Could not get video count from playlist", e); } try { return Long.parseLong(Utils.removeNonDigitCharacters(input)); } catch (NumberFormatException e) { // When there's no videos in a playlist, there's no number in the "innerHtml", // all characters that is not a number is removed, so we try to parse a empty string if (!input.isEmpty()) { return 0; } else { throw new ParsingException("Could not handle input: " + input, e); } } } @Nonnull @Override public StreamInfoItemsCollector getStreams() throws IOException, ExtractionException { StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); Element tbody = doc.select("tbody[id=\"pl-load-more-destination\"]").first(); collectStreamsFrom(collector, tbody); return collector; } @Override public InfoItemPage getInfoItemPage() throws IOException, ExtractionException { if (!hasNextPage()) { throw new ExtractionException("Playlist doesn't have more streams"); } StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); setupNextStreamsAjax(NewPipe.getDownloader()); collectStreamsFrom(collector, nextStreamsAjax.select("tbody[id=\"pl-load-more-destination\"]").first()); return new InfoItemPage(collector, nextPageUrl); } private void setupNextStreamsAjax(Downloader downloader) throws IOException, ReCaptchaException, ParsingException { String ajaxDataRaw = downloader.download(nextPageUrl); try { JsonObject ajaxData = JsonParser.object().from(ajaxDataRaw); String htmlDataRaw = "