-
-
Notifications
You must be signed in to change notification settings - Fork 518
Extractor for youtube mix (auto-generated playlist) #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d880594
b8eacff
7fabcf4
8c3e450
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| package org.schabi.newpipe.extractor.services.youtube.extractors; | ||
|
|
||
| import java.io.IOException; | ||
| import javax.annotation.Nonnull; | ||
| import javax.annotation.Nullable; | ||
| import org.jsoup.nodes.Document; | ||
| import org.jsoup.nodes.Element; | ||
| 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.LinkHandlerFactory; | ||
| 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.services.youtube.linkHandler.YoutubeParsingHelper; | ||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||
| import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; | ||
|
|
||
| /** | ||
| * 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 Document doc; | ||
|
|
||
| public YoutubeMixPlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) { | ||
| super(service, linkHandler); | ||
| } | ||
|
|
||
| @Override | ||
| public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException { | ||
| final String url = getUrl(); | ||
| final Response response = downloader.get(url, getExtractorLocalization()); | ||
| doc = YoutubeParsingHelper.parseAndCheckPage(url, response); | ||
| } | ||
|
|
||
| @Nonnull | ||
| @Override | ||
| public String getName() throws ParsingException { | ||
| try { | ||
| return doc.select("div[class=\"playlist-info\"] h3[class=\"playlist-title\"]").first().text(); | ||
| } catch (Exception e) { | ||
| throw new ParsingException("Could not get playlist name", e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String getThumbnailUrl() throws ParsingException { | ||
| try { | ||
| Element li = doc.select("ol[class*=\"playlist-videos-list\"] li").first(); | ||
| String videoId = li.attr("data-video-id"); | ||
| if (videoId != null && !videoId.isEmpty()) { | ||
| //higher quality | ||
| return getThumbnailUrlFromId(videoId); | ||
| } else { | ||
| //lower quality | ||
| return doc.select("ol[class*=\"playlist-videos-list\"] li").first() | ||
| .attr("data-thumbnail-url"); | ||
| } | ||
| } 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 | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| public String getUploaderAvatarUrl() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As they are generated by YouTube, shouldn't you return YouTube logo (or censored version)?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably need to hardcode the url, since i can't find one in the crawled html. |
||
| //Youtube mix are auto-generated | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| public long getStreamCount() { | ||
| // Auto-generated playlist always start with 25 videos and are endless | ||
| // But the html doesn't have a continuation url | ||
| return doc.select("ol[class*=\"playlist-videos-list\"] li").size(); | ||
| } | ||
|
|
||
| @Nonnull | ||
| @Override | ||
| public InfoItemsPage<StreamInfoItem> getInitialPage() { | ||
| StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); | ||
| Element ol = doc.select("ol[class*=\"playlist-videos-list\"]").first(); | ||
| collectStreamsFrom(collector, ol); | ||
| return new InfoItemsPage<>(collector, getNextPageUrl()); | ||
| } | ||
|
|
||
| @Override | ||
| public String getNextPageUrl() { | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| public InfoItemsPage<StreamInfoItem> getPage(final String pageUrl) { | ||
| //Continuations are not implemented | ||
| return null; | ||
| } | ||
|
|
||
| private void collectStreamsFrom( | ||
| @Nonnull StreamInfoItemsCollector collector, | ||
| @Nullable Element element) { | ||
| collector.reset(); | ||
|
|
||
| if (element == null) { | ||
| return; | ||
| } | ||
|
|
||
| final LinkHandlerFactory streamLinkHandlerFactory = getService().getStreamLHFactory(); | ||
| final TimeAgoParser timeAgoParser = getTimeAgoParser(); | ||
|
|
||
| for (final Element li : element.children()) { | ||
|
|
||
| collector.commit(new YoutubeStreamInfoItemExtractor(li, timeAgoParser) { | ||
|
|
||
| @Override | ||
| public boolean isAd() { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public String getUrl() throws ParsingException { | ||
| try { | ||
| return streamLinkHandlerFactory.fromId(li.attr("data-video-id")).getUrl(); | ||
| } catch (Exception e) { | ||
| throw new ParsingException("Could not get web page url for the video", e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String getName() throws ParsingException { | ||
| try { | ||
| return li.attr("data-video-title"); | ||
| } catch (Exception e) { | ||
| throw new ParsingException("Could not get name", e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public long getDuration() { | ||
| //Not present in doc | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public String getUploaderName() throws ParsingException { | ||
| String uploaderName = li.attr("data-video-username"); | ||
| if (uploaderName == null || uploaderName.isEmpty()) { | ||
| throw new ParsingException("Could not get uploader name"); | ||
| } else { | ||
| return uploaderName; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String getUploaderUrl() { | ||
| //Not present in doc | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| public String getTextualUploadDate() { | ||
| //Not present in doc | ||
| return ""; | ||
| } | ||
|
|
||
| @Override | ||
| public long getViewCount() { | ||
| return -1; | ||
| } | ||
|
|
||
| @Override | ||
| public String getThumbnailUrl() throws ParsingException { | ||
| try { | ||
| return getThumbnailUrlFromId(streamLinkHandlerFactory.fromUrl(getUrl()).getId()); | ||
| } catch (Exception e) { | ||
| throw new ParsingException("Could not get thumbnail url", e); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private String getThumbnailUrlFromId(String videoId) { | ||
| return "https://i.ytimg.com/vi/" + videoId + "/hqdefault.jpg"; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return YouTube instead of empty String.

See this screenshot, on YouTube website (look at top left):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have that YouTube string marked in you image when i open the site.
But that could also be changed in the app, since there was already a PR to deal with playlists without uploader. Just the String resource would need to be changed.
TeamNewPipe/NewPipe#2724