44import com .grack .nanojson .JsonObject ;
55
66import org .schabi .newpipe .extractor .ListExtractor ;
7+ import org .schabi .newpipe .extractor .Page ;
78import org .schabi .newpipe .extractor .StreamingService ;
89import org .schabi .newpipe .extractor .downloader .Downloader ;
10+ import org .schabi .newpipe .extractor .downloader .Response ;
911import org .schabi .newpipe .extractor .exceptions .ExtractionException ;
1012import org .schabi .newpipe .extractor .exceptions .ParsingException ;
1113import org .schabi .newpipe .extractor .linkhandler .ListLinkHandler ;
1517import org .schabi .newpipe .extractor .stream .StreamInfoItemsCollector ;
1618
1719import java .io .IOException ;
20+ import java .util .Collections ;
21+ import java .util .List ;
1822
1923import javax .annotation .Nonnull ;
2024import javax .annotation .Nullable ;
2125
26+ import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .extractCookieValue ;
2227import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getJsonResponse ;
28+ import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getResponse ;
2329import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getUrlFromNavigationEndpoint ;
30+ import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .toJsonArray ;
2431
2532/**
26- * A YoutubePlaylistExtractor for a mix (auto-generated playlist). It handles urls in the format of
27- * "youtube.com/watch?v=videoId&list=playlistId"
33+ * A {@link YoutubePlaylistExtractor} for a mix (auto-generated playlist).
34+ * It handles URLs in the format of
35+ * {@code youtube.com/watch?v=videoId&list=playlistId}
2836 */
2937public class YoutubeMixPlaylistExtractor extends PlaylistExtractor {
3038
39+ /**
40+ * YouTube identifies mixes based on this cookie. With this information it can generate
41+ * continuations without duplicates.
42+ */
43+ private static final String COOKIE_NAME = "VISITOR_INFO1_LIVE" ;
44+
3145 private JsonObject initialData ;
3246 private JsonObject playlistData ;
47+ private String cookieValue ;
3348
34- public YoutubeMixPlaylistExtractor (StreamingService service , ListLinkHandler linkHandler ) {
49+ public YoutubeMixPlaylistExtractor (final StreamingService service ,
50+ final ListLinkHandler linkHandler ) {
3551 super (service , linkHandler );
3652 }
3753
3854 @ Override
39- public void onFetchPage (@ Nonnull Downloader downloader )
40- throws IOException , ExtractionException {
55+ public void onFetchPage (@ Nonnull final Downloader downloader )
56+ throws IOException , ExtractionException {
4157 final String url = getUrl () + "&pbj=1" ;
42- final JsonArray ajaxJson = getJsonResponse (url , getExtractorLocalization ());
58+ final Response response = getResponse (url , getExtractorLocalization ());
59+ final JsonArray ajaxJson = toJsonArray (response .responseBody ());
4360 initialData = ajaxJson .getObject (3 ).getObject ("response" );
4461 playlistData = initialData .getObject ("contents" ).getObject ("twoColumnWatchNextResults" )
45- .getObject ("playlist" ).getObject ("playlist" );
62+ .getObject ("playlist" ).getObject ("playlist" );
63+ cookieValue = extractCookieValue (COOKIE_NAME , response );
4664 }
4765
4866 @ Nonnull
@@ -58,16 +76,15 @@ public String getName() throws ParsingException {
5876 @ Override
5977 public String getThumbnailUrl () throws ParsingException {
6078 try {
61- final String playlistId = playlistData .getString ("playlistId" );
79+ return getThumbnailUrlFromPlaylistId (playlistData .getString ("playlistId" ));
80+ } catch (final Exception e ) {
6281 try {
63- return getThumbnailUrlFromPlaylistId (playlistId );
64- } catch (ParsingException e ) {
6582 //fallback to thumbnail of current video. Always the case for channel mix
6683 return getThumbnailUrlFromVideoId (
6784 initialData .getObject ("currentVideoEndpoint" ).getObject ("watchEndpoint" )
6885 .getString ("videoId" ));
86+ } catch (final Exception ignored ) {
6987 }
70- } catch (Exception e ) {
7188 throw new ParsingException ("Could not get playlist thumbnail" , e );
7289 }
7390 }
@@ -104,63 +121,66 @@ public long getStreamCount() {
104121 @ Nonnull
105122 @ Override
106123 public InfoItemsPage <StreamInfoItem > getInitialPage () throws ExtractionException {
107- StreamInfoItemsCollector collector = new StreamInfoItemsCollector (getServiceId ());
124+ final StreamInfoItemsCollector collector = new StreamInfoItemsCollector (getServiceId ());
108125 collectStreamsFrom (collector , playlistData .getArray ("contents" ));
109- return new InfoItemsPage <>(collector , getNextPageUrl ());
126+ return new InfoItemsPage <>(collector ,
127+ new Page (getNextPageUrl (), Collections .singletonMap (COOKIE_NAME , cookieValue )));
110128 }
111129
112- @ Override
113- public String getNextPageUrl () throws ExtractionException {
130+ private String getNextPageUrl () throws ExtractionException {
114131 return getNextPageUrlFrom (playlistData );
115132 }
116133
117- private String getNextPageUrlFrom (JsonObject playlistData ) throws ExtractionException {
118- final JsonObject lastStream = ((JsonObject ) playlistData .getArray ("contents" )
119- .get (playlistData .getArray ("contents" ).size () - 1 ));
134+ private String getNextPageUrlFrom (final JsonObject playlistJson ) throws ExtractionException {
135+ final JsonObject lastStream = ((JsonObject ) playlistJson .getArray ("contents" )
136+ .get (playlistJson .getArray ("contents" ).size () - 1 ));
120137 if (lastStream == null || lastStream .getObject ("playlistPanelVideoRenderer" ) == null ) {
121138 throw new ExtractionException ("Could not extract next page url" );
122139 }
123- //Index of video in mix is missing, but adding it doesn't appear to have any effect.
124- //And since the index needs to be tracked by us, it is left out
140+
125141 return getUrlFromNavigationEndpoint (
126- lastStream .getObject ("playlistPanelVideoRenderer" ).getObject ("navigationEndpoint" ))
127- + "&pbj=1" ;
142+ lastStream .getObject ("playlistPanelVideoRenderer" ).getObject ("navigationEndpoint" ))
143+ + "&pbj=1" ;
128144 }
129145
130146 @ Override
131- public InfoItemsPage <StreamInfoItem > getPage (final String pageUrl )
147+ public InfoItemsPage <StreamInfoItem > getPage (final Page page )
132148 throws ExtractionException , IOException {
133- if (pageUrl == null || pageUrl .isEmpty ()) {
149+ if (page == null || page . getUrl () .isEmpty ()) {
134150 throw new ExtractionException (
135151 new IllegalArgumentException ("Page url is empty or null" ));
136152 }
137153
138- StreamInfoItemsCollector collector = new StreamInfoItemsCollector (getServiceId ());
139- final JsonArray ajaxJson = getJsonResponse (pageUrl , getExtractorLocalization ());
140- JsonObject playlistData =
141- ajaxJson .getObject (3 ).getObject ("response" ).getObject ("contents" )
142- .getObject ("twoColumnWatchNextResults" ).getObject ("playlist" )
143- .getObject ("playlist" );
144- final JsonArray streams = playlistData .getArray ("contents" );
145- //Because continuation requests are created with the last video of previous request as start
146- streams .remove (0 );
147- collectStreamsFrom (collector , streams );
148- return new InfoItemsPage <>(collector , getNextPageUrlFrom (playlistData ));
154+ final JsonArray ajaxJson = getJsonResponse (page , getExtractorLocalization ());
155+ final JsonObject playlistJson =
156+ ajaxJson .getObject (3 ).getObject ("response" ).getObject ("contents" )
157+ .getObject ("twoColumnWatchNextResults" ).getObject ("playlist" )
158+ .getObject ("playlist" );
159+ final JsonArray allStreams = playlistJson .getArray ("contents" );
160+ // Sublist because youtube returns up to 24 previous streams in the mix
161+ // +1 because the stream of "currentIndex" was already extracted in previous request
162+ final List <Object > newStreams =
163+ allStreams .subList (playlistJson .getInt ("currentIndex" ) + 1 , allStreams .size ());
164+
165+ final StreamInfoItemsCollector collector = new StreamInfoItemsCollector (getServiceId ());
166+ collectStreamsFrom (collector , newStreams );
167+ return new InfoItemsPage <>(collector ,
168+ new Page (getNextPageUrlFrom (playlistJson ), page .getCookies ()));
149169 }
150170
151171 private void collectStreamsFrom (
152- @ Nonnull StreamInfoItemsCollector collector ,
153- @ Nullable JsonArray streams ) {
172+ @ Nonnull final StreamInfoItemsCollector collector ,
173+ @ Nullable final List < Object > streams ) {
154174
155175 if (streams == null ) {
156176 return ;
157177 }
158178
159179 final TimeAgoParser timeAgoParser = getTimeAgoParser ();
160180
161- for (Object stream : streams ) {
181+ for (final Object stream : streams ) {
162182 if (stream instanceof JsonObject ) {
163- JsonObject streamInfo = ((JsonObject ) stream )
183+ final JsonObject streamInfo = ((JsonObject ) stream )
164184 .getObject ("playlistPanelVideoRenderer" );
165185 if (streamInfo != null ) {
166186 collector .commit (new YoutubeStreamInfoItemExtractor (streamInfo , timeAgoParser ));
@@ -169,7 +189,7 @@ private void collectStreamsFrom(
169189 }
170190 }
171191
172- private String getThumbnailUrlFromPlaylistId (String playlistId ) throws ParsingException {
192+ private String getThumbnailUrlFromPlaylistId (final String playlistId ) throws ParsingException {
173193 final String videoId ;
174194 if (playlistId .startsWith ("RDMM" )) {
175195 videoId = playlistId .substring (4 );
@@ -184,7 +204,7 @@ private String getThumbnailUrlFromPlaylistId(String playlistId) throws ParsingEx
184204 return getThumbnailUrlFromVideoId (videoId );
185205 }
186206
187- private String getThumbnailUrlFromVideoId (String videoId ) {
207+ private String getThumbnailUrlFromVideoId (final String videoId ) {
188208 return "https://i.ytimg.com/vi/" + videoId + "/hqdefault.jpg" ;
189209 }
190210
0 commit comments