88import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getImagesFromThumbnailsArray ;
99import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getUrlFromNavigationEndpoint ;
1010import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .prepareDesktopJsonBuilder ;
11+ import static org .schabi .newpipe .extractor .services .youtube .protos .playlist .PlaylistProtobufContinuation .ContinuationParams ;
12+ import static org .schabi .newpipe .extractor .services .youtube .protos .playlist .PlaylistProtobufContinuation .PlaylistContinuation ;
1113import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
1214
1315import com .grack .nanojson .JsonArray ;
3335
3436import java .io .IOException ;
3537import java .nio .charset .StandardCharsets ;
38+ import java .util .Base64 ;
3639import java .util .List ;
3740
3841import javax .annotation .Nonnull ;
4144public class YoutubePlaylistExtractor extends PlaylistExtractor {
4245 // Names of some objects in JSON response frequently used in this class
4346 private static final String PLAYLIST_VIDEO_RENDERER = "playlistVideoRenderer" ;
44- private static final String PLAYLIST_VIDEO_LIST_RENDERER = "playlistVideoListRenderer" ;
45- private static final String RICH_GRID_RENDERER = "richGridRenderer" ;
4647 private static final String RICH_ITEM_RENDERER = "richItemRenderer" ;
4748 private static final String REEL_ITEM_RENDERER = "reelItemRenderer" ;
4849 private static final String SIDEBAR = "sidebar" ;
50+ private static final String HEADER = "header" ;
4951 private static final String VIDEO_OWNER_RENDERER = "videoOwnerRenderer" ;
52+ private static final String MICROFORMAT = "microformat" ;
53+ // Continuation properties requesting first page and showing unavailable videos
54+ private static final String PLAYLIST_CONTINUATION_PROPERTIES_BASE64 = "CADCBgIIAA%3D%3D" ;
5055
51- private JsonObject browseResponse ;
56+ private JsonObject browseMetadataResponse ;
57+ private JsonObject initialBrowseContinuationResponse ;
5258
5359 private JsonObject playlistInfo ;
5460 private JsonObject uploaderInfo ;
@@ -64,17 +70,40 @@ public YoutubePlaylistExtractor(final StreamingService service,
6470 @ Override
6571 public void onFetchPage (@ Nonnull final Downloader downloader ) throws IOException ,
6672 ExtractionException {
73+ final String playlistId = getId ();
74+
6775 final Localization localization = getExtractorLocalization ();
6876 final byte [] body = JsonWriter .string (prepareDesktopJsonBuilder (localization ,
6977 getExtractorContentCountry ())
70- .value ("browseId" , "VL" + getId () )
78+ .value ("browseId" , "VL" + playlistId )
7179 .value ("params" , "wgYCCAA%3D" ) // Show unavailable videos
7280 .done ())
7381 .getBytes (StandardCharsets .UTF_8 );
7482
75- browseResponse = getJsonPostResponse ("browse" , body , localization );
76- YoutubeParsingHelper .defaultAlertsCheck (browseResponse );
83+ browseMetadataResponse = getJsonPostResponse ("browse" ,
84+ List .of ("$fields=" + SIDEBAR + "," + HEADER + "," + MICROFORMAT + ",alerts" ),
85+ body ,
86+ localization );
87+
88+ YoutubeParsingHelper .defaultAlertsCheck (browseMetadataResponse );
7789 isNewPlaylistInterface = checkIfResponseIsNewPlaylistInterface ();
90+
91+ final PlaylistContinuation playlistContinuation = PlaylistContinuation .newBuilder ()
92+ .setParameters (ContinuationParams .newBuilder ()
93+ .setBrowseId ("VL" + playlistId )
94+ .setPlaylistId (playlistId )
95+ .setContinuationProperties (PLAYLIST_CONTINUATION_PROPERTIES_BASE64 )
96+ .build ())
97+ .build ();
98+
99+ initialBrowseContinuationResponse = getJsonPostResponse ("browse" ,
100+ JsonWriter .string (prepareDesktopJsonBuilder (localization ,
101+ getExtractorContentCountry ())
102+ .value ("continuation" , Utils .encodeUrlUtf8 (Base64 .getUrlEncoder ()
103+ .encodeToString (playlistContinuation .toByteArray ())))
104+ .done ())
105+ .getBytes (StandardCharsets .UTF_8 ),
106+ localization );
78107 }
79108
80109 /**
@@ -93,13 +122,13 @@ public void onFetchPage(@Nonnull final Downloader downloader) throws IOException
93122 */
94123 private boolean checkIfResponseIsNewPlaylistInterface () {
95124 // The "old" playlist UI can be also returned with the new one
96- return browseResponse .has ("header" ) && !browseResponse .has (SIDEBAR );
125+ return browseMetadataResponse .has (HEADER ) && !browseMetadataResponse .has (SIDEBAR );
97126 }
98127
99128 @ Nonnull
100129 private JsonObject getUploaderInfo () throws ParsingException {
101130 if (uploaderInfo == null ) {
102- uploaderInfo = browseResponse .getObject (SIDEBAR )
131+ uploaderInfo = browseMetadataResponse .getObject (SIDEBAR )
103132 .getObject ("playlistSidebarRenderer" )
104133 .getArray ("items" )
105134 .stream ()
@@ -121,7 +150,7 @@ private JsonObject getUploaderInfo() throws ParsingException {
121150 @ Nonnull
122151 private JsonObject getPlaylistInfo () throws ParsingException {
123152 if (playlistInfo == null ) {
124- playlistInfo = browseResponse .getObject (SIDEBAR )
153+ playlistInfo = browseMetadataResponse .getObject (SIDEBAR )
125154 .getObject ("playlistSidebarRenderer" )
126155 .getArray ("items" )
127156 .stream ()
@@ -139,7 +168,7 @@ private JsonObject getPlaylistInfo() throws ParsingException {
139168 @ Nonnull
140169 private JsonObject getPlaylistHeader () {
141170 if (playlistHeader == null ) {
142- playlistHeader = browseResponse .getObject ("header" )
171+ playlistHeader = browseMetadataResponse .getObject (HEADER )
143172 .getObject ("playlistHeaderRenderer" );
144173 }
145174
@@ -154,7 +183,7 @@ public String getName() throws ParsingException {
154183 return name ;
155184 }
156185
157- return browseResponse .getObject ("microformat" )
186+ return browseMetadataResponse .getObject (MICROFORMAT )
158187 .getObject ("microformatDataRenderer" )
159188 .getString ("title" );
160189 }
@@ -180,7 +209,7 @@ public List<Image> getThumbnails() throws ParsingException {
180209 }
181210
182211 // This data structure is returned in both layouts
183- final JsonArray microFormatThumbnailsArray = browseResponse .getObject ("microformat" )
212+ final JsonArray microFormatThumbnailsArray = browseMetadataResponse .getObject (MICROFORMAT )
184213 .getObject ("microformatDataRenderer" )
185214 .getObject ("thumbnail" )
186215 .getArray ("thumbnails" );
@@ -302,45 +331,16 @@ public Description getDescription() throws ParsingException {
302331 @ Override
303332 public InfoItemsPage <StreamInfoItem > getInitialPage () throws IOException , ExtractionException {
304333 final StreamInfoItemsCollector collector = new StreamInfoItemsCollector (getServiceId ());
305- Page nextPage = null ;
306334
307- final JsonArray contents = browseResponse .getObject ("contents" )
308- .getObject ("twoColumnBrowseResultsRenderer" )
309- .getArray ("tabs" )
335+ final JsonArray initialItems = initialBrowseContinuationResponse
336+ .getArray ("onResponseReceivedActions" )
310337 .getObject (0 )
311- .getObject ("tabRenderer" )
312- .getObject ("content" )
313- .getObject ("sectionListRenderer" )
314- .getArray ("contents" );
315-
316- final JsonObject videoPlaylistObject = contents .stream ()
317- .filter (JsonObject .class ::isInstance )
318- .map (JsonObject .class ::cast )
319- .map (content -> content .getObject ("itemSectionRenderer" )
320- .getArray ("contents" )
321- .getObject (0 ))
322- .filter (content -> content .has (PLAYLIST_VIDEO_LIST_RENDERER )
323- || content .has (RICH_GRID_RENDERER ))
324- .findFirst ()
325- .orElse (null );
326-
327- if (videoPlaylistObject != null ) {
328- final JsonObject renderer ;
329- if (videoPlaylistObject .has (PLAYLIST_VIDEO_LIST_RENDERER )) {
330- renderer = videoPlaylistObject .getObject (PLAYLIST_VIDEO_LIST_RENDERER );
331- } else if (videoPlaylistObject .has (RICH_GRID_RENDERER )) {
332- renderer = videoPlaylistObject .getObject (RICH_GRID_RENDERER );
333- } else {
334- return new InfoItemsPage <>(collector , null );
335- }
336-
337- final JsonArray videosArray = renderer .getArray ("contents" );
338- collectStreamsFrom (collector , videosArray );
338+ .getObject ("reloadContinuationItemsCommand" )
339+ .getArray ("continuationItems" );
339340
340- nextPage = getNextPageFrom (videosArray );
341- }
341+ collectStreamsFrom (collector , initialItems );
342342
343- return new InfoItemsPage <>(collector , nextPage );
343+ return new InfoItemsPage <>(collector , getNextPageFrom ( initialItems ) );
344344 }
345345
346346 @ Override
0 commit comments