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+ private static final String PLAYLIST_CONTINUATION_PROPERTIES_BASE64 = "CADCBgIIAA%3D%3D" ;
5054
51- private JsonObject browseResponse ;
55+ private JsonObject browseMetadataResponse ;
56+ private JsonObject initialBrowseContinuationResponse ;
5257
5358 private JsonObject playlistInfo ;
5459 private JsonObject uploaderInfo ;
@@ -64,17 +69,39 @@ public YoutubePlaylistExtractor(final StreamingService service,
6469 @ Override
6570 public void onFetchPage (@ Nonnull final Downloader downloader ) throws IOException ,
6671 ExtractionException {
72+ final String playlistId = getId ();
73+
6774 final Localization localization = getExtractorLocalization ();
6875 final byte [] body = JsonWriter .string (prepareDesktopJsonBuilder (localization ,
6976 getExtractorContentCountry ())
70- .value ("browseId" , "VL" + getId () )
77+ .value ("browseId" , "VL" + playlistId )
7178 .value ("params" , "wgYCCAA%3D" ) // Show unavailable videos
7279 .done ())
7380 .getBytes (StandardCharsets .UTF_8 );
7481
75- browseResponse = getJsonPostResponse ("browse" , body , localization );
76- YoutubeParsingHelper .defaultAlertsCheck (browseResponse );
82+ browseMetadataResponse = getJsonPostResponse ("browse" ,
83+ List .of ("$fields=" + SIDEBAR + "," + HEADER + "," + MICROFORMAT ), body ,
84+ localization );
85+
86+ YoutubeParsingHelper .defaultAlertsCheck (browseMetadataResponse );
7787 isNewPlaylistInterface = checkIfResponseIsNewPlaylistInterface ();
88+
89+ final PlaylistContinuation playlistContinuation = PlaylistContinuation .newBuilder ()
90+ .setParameters (ContinuationParams .newBuilder ()
91+ .setBrowseId ("VL" + playlistId )
92+ .setPlaylistId (playlistId )
93+ .setContinuationProperties (PLAYLIST_CONTINUATION_PROPERTIES_BASE64 )
94+ .build ())
95+ .build ();
96+
97+ initialBrowseContinuationResponse = getJsonPostResponse ("browse" ,
98+ JsonWriter .string (prepareDesktopJsonBuilder (localization ,
99+ getExtractorContentCountry ())
100+ .value ("continuation" , Utils .encodeUrlUtf8 (Base64 .getUrlEncoder ()
101+ .encodeToString (playlistContinuation .toByteArray ())))
102+ .done ())
103+ .getBytes (StandardCharsets .UTF_8 ),
104+ localization );
78105 }
79106
80107 /**
@@ -93,13 +120,13 @@ public void onFetchPage(@Nonnull final Downloader downloader) throws IOException
93120 */
94121 private boolean checkIfResponseIsNewPlaylistInterface () {
95122 // The "old" playlist UI can be also returned with the new one
96- return browseResponse .has ("header" ) && !browseResponse .has (SIDEBAR );
123+ return browseMetadataResponse .has (HEADER ) && !browseMetadataResponse .has (SIDEBAR );
97124 }
98125
99126 @ Nonnull
100127 private JsonObject getUploaderInfo () throws ParsingException {
101128 if (uploaderInfo == null ) {
102- uploaderInfo = browseResponse .getObject (SIDEBAR )
129+ uploaderInfo = browseMetadataResponse .getObject (SIDEBAR )
103130 .getObject ("playlistSidebarRenderer" )
104131 .getArray ("items" )
105132 .stream ()
@@ -121,7 +148,7 @@ private JsonObject getUploaderInfo() throws ParsingException {
121148 @ Nonnull
122149 private JsonObject getPlaylistInfo () throws ParsingException {
123150 if (playlistInfo == null ) {
124- playlistInfo = browseResponse .getObject (SIDEBAR )
151+ playlistInfo = browseMetadataResponse .getObject (SIDEBAR )
125152 .getObject ("playlistSidebarRenderer" )
126153 .getArray ("items" )
127154 .stream ()
@@ -139,7 +166,7 @@ private JsonObject getPlaylistInfo() throws ParsingException {
139166 @ Nonnull
140167 private JsonObject getPlaylistHeader () {
141168 if (playlistHeader == null ) {
142- playlistHeader = browseResponse .getObject ("header" )
169+ playlistHeader = browseMetadataResponse .getObject (HEADER )
143170 .getObject ("playlistHeaderRenderer" );
144171 }
145172
@@ -154,7 +181,7 @@ public String getName() throws ParsingException {
154181 return name ;
155182 }
156183
157- return browseResponse .getObject ("microformat" )
184+ return browseMetadataResponse .getObject (MICROFORMAT )
158185 .getObject ("microformatDataRenderer" )
159186 .getString ("title" );
160187 }
@@ -180,7 +207,7 @@ public List<Image> getThumbnails() throws ParsingException {
180207 }
181208
182209 // This data structure is returned in both layouts
183- final JsonArray microFormatThumbnailsArray = browseResponse .getObject ("microformat" )
210+ final JsonArray microFormatThumbnailsArray = browseMetadataResponse .getObject (MICROFORMAT )
184211 .getObject ("microformatDataRenderer" )
185212 .getObject ("thumbnail" )
186213 .getArray ("thumbnails" );
@@ -302,45 +329,16 @@ public Description getDescription() throws ParsingException {
302329 @ Override
303330 public InfoItemsPage <StreamInfoItem > getInitialPage () throws IOException , ExtractionException {
304331 final StreamInfoItemsCollector collector = new StreamInfoItemsCollector (getServiceId ());
305- Page nextPage = null ;
306332
307- final JsonArray contents = browseResponse .getObject ("contents" )
308- .getObject ("twoColumnBrowseResultsRenderer" )
309- .getArray ("tabs" )
333+ final JsonArray initialItems = initialBrowseContinuationResponse
334+ .getArray ("onResponseReceivedActions" )
310335 .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 );
336+ .getObject ("reloadContinuationItemsCommand" )
337+ .getArray ("continuationItems" );
339338
340- nextPage = getNextPageFrom (videosArray );
341- }
339+ collectStreamsFrom (collector , initialItems );
342340
343- return new InfoItemsPage <>(collector , nextPage );
341+ return new InfoItemsPage <>(collector , getNextPageFrom ( initialItems ) );
344342 }
345343
346344 @ Override
0 commit comments