3737 * A {@link ChannelTabExtractor} implementation for the YouTube service.
3838 *
3939 * <p>
40- * It currently supports {@code Videos}, {@code Shorts}, {@code Live}, {@code Playlists} and
41- * {@code Channels} tabs.
40+ * It currently supports {@code Videos}, {@code Shorts}, {@code Live}, {@code Playlists},
41+ * {@code Albums} and {@code Channels} tabs.
4242 * </p>
4343 */
4444public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@@ -60,6 +60,8 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
6060 private String channelId ;
6161 @ Nullable
6262 private String visitorData ;
63+ @ SuppressWarnings ("OptionalUsedAsFieldOrParameterType" )
64+ private Optional <YoutubeChannelHelper .ChannelHeader > channelHeader ;
6365
6466 public YoutubeChannelTabExtractor (final StreamingService service ,
6567 final ListLinkHandler linkHandler ) {
@@ -89,14 +91,15 @@ private String getChannelTabsParameters() throws ParsingException {
8991 @ Override
9092 public void onFetchPage (@ Nonnull final Downloader downloader ) throws IOException ,
9193 ExtractionException {
92- channelId = resolveChannelId (super .getId ());
94+ final String channelIdFromId = resolveChannelId (super .getId ());
9395
9496 final String params = getChannelTabsParameters ();
9597
96- final YoutubeChannelHelper .ChannelResponseData data = getChannelResponse (channelId ,
98+ final YoutubeChannelHelper .ChannelResponseData data = getChannelResponse (channelIdFromId ,
9799 params , getExtractorLocalization (), getExtractorContentCountry ());
98100
99101 jsonResponse = data .jsonResponse ;
102+ channelHeader = YoutubeChannelHelper .getChannelHeader (jsonResponse );
100103 channelId = data .channelId ;
101104 if (useVisitorData ) {
102105 visitorData = jsonResponse .getObject ("responseContext" ).getString ("visitorData" );
@@ -204,18 +207,27 @@ public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionEx
204207 }
205208 }
206209
210+ final VerifiedStatus verifiedStatus = channelHeader .flatMap (header ->
211+ YoutubeChannelHelper .isChannelVerified (header )
212+ ? Optional .of (VerifiedStatus .VERIFIED )
213+ : Optional .of (VerifiedStatus .UNVERIFIED ))
214+ .orElse (VerifiedStatus .UNKNOWN );
215+
207216 // If a channel tab is fetched, the next page requires channel ID and name, as channel
208217 // streams don't have their channel specified.
209218 // We also need to set the visitor data here when it should be enabled, as it is required
210219 // to get continuations on some channel tabs, and we need a way to pass it between pages
211- final List <String > channelIds = useVisitorData && !isNullOrEmpty (visitorData )
212- ? List .of (getChannelName (), getUrl (), visitorData )
213- : List .of (getChannelName (), getUrl ());
220+ final String channelName = getChannelName ();
221+ final String channelUrl = getUrl ();
214222
215- final JsonObject continuation = collectItemsFrom (collector , items , channelIds )
223+ final JsonObject continuation = collectItemsFrom (collector , items , verifiedStatus ,
224+ channelName , channelUrl )
216225 .orElse (null );
217226
218- final Page nextPage = getNextPageFrom (continuation , channelIds );
227+ final Page nextPage = getNextPageFrom (continuation ,
228+ useVisitorData && !isNullOrEmpty (visitorData )
229+ ? List .of (channelName , channelUrl , verifiedStatus .toString (), visitorData )
230+ : List .of (channelName , channelUrl , verifiedStatus .toString ()));
219231
220232 return new InfoItemsPage <>(collector , nextPage );
221233 }
@@ -281,123 +293,178 @@ Optional<JsonObject> getTabData() {
281293 private Optional <JsonObject > collectItemsFrom (@ Nonnull final MultiInfoItemsCollector collector ,
282294 @ Nonnull final JsonArray items ,
283295 @ Nonnull final List <String > channelIds ) {
296+ final String channelName ;
297+ final String channelUrl ;
298+ VerifiedStatus verifiedStatus ;
299+
300+ if (channelIds .size () >= 3 ) {
301+ channelName = channelIds .get (0 );
302+ channelUrl = channelIds .get (1 );
303+ try {
304+ verifiedStatus = VerifiedStatus .valueOf (channelIds .get (2 ));
305+ } catch (final IllegalArgumentException e ) {
306+ // An IllegalArgumentException can be thrown if someone passes a third channel ID
307+ // which is not of the enum type in the getPage method, use the UNKNOWN
308+ // VerifiedStatus enum value in this case
309+ verifiedStatus = VerifiedStatus .UNKNOWN ;
310+ }
311+ } else {
312+ channelName = null ;
313+ channelUrl = null ;
314+ verifiedStatus = VerifiedStatus .UNKNOWN ;
315+ }
316+
317+ return collectItemsFrom (collector , items , verifiedStatus , channelName , channelUrl );
318+ }
319+
320+ private Optional <JsonObject > collectItemsFrom (@ Nonnull final MultiInfoItemsCollector collector ,
321+ @ Nonnull final JsonArray items ,
322+ @ Nonnull final VerifiedStatus verifiedStatus ,
323+ @ Nullable final String channelName ,
324+ @ Nullable final String channelUrl ) {
284325 return items .stream ()
285326 .filter (JsonObject .class ::isInstance )
286327 .map (JsonObject .class ::cast )
287- .map (item -> collectItem (collector , item , channelIds ))
328+ .map (item -> collectItem (
329+ collector , item , verifiedStatus , channelName , channelUrl ))
288330 .reduce (Optional .empty (), (c1 , c2 ) -> c1 .or (() -> c2 ));
289331 }
290332
291333 private Optional <JsonObject > collectItem (@ Nonnull final MultiInfoItemsCollector collector ,
292334 @ Nonnull final JsonObject item ,
293- @ Nonnull final List <String > channelIds ) {
335+ @ Nonnull final VerifiedStatus channelVerifiedStatus ,
336+ @ Nullable final String channelName ,
337+ @ Nullable final String channelUrl ) {
294338 final TimeAgoParser timeAgoParser = getTimeAgoParser ();
295339
296340 if (item .has ("richItemRenderer" )) {
297341 final JsonObject richItem = item .getObject ("richItemRenderer" )
298342 .getObject ("content" );
299343
300344 if (richItem .has ("videoRenderer" )) {
301- getCommitVideoConsumer (collector , timeAgoParser , channelIds ,
302- richItem . getObject ( "videoRenderer" ) );
345+ commitVideo (collector , timeAgoParser , richItem . getObject ( "videoRenderer" ) ,
346+ channelVerifiedStatus , channelName , channelUrl );
303347 } else if (richItem .has ("reelItemRenderer" )) {
304- getCommitReelItemConsumer (collector , channelIds ,
305- richItem . getObject ( "reelItemRenderer" ) );
348+ commitReel (collector , richItem . getObject ( "reelItemRenderer" ) ,
349+ channelVerifiedStatus , channelName , channelUrl );
306350 } else if (richItem .has ("playlistRenderer" )) {
307- getCommitPlaylistConsumer (collector , channelIds ,
308- richItem . getObject ( "playlistRenderer" ) );
351+ commitPlaylist (collector , richItem . getObject ( "playlistRenderer" ) ,
352+ channelVerifiedStatus , channelName , channelUrl );
309353 }
310354 } else if (item .has ("gridVideoRenderer" )) {
311- getCommitVideoConsumer (collector , timeAgoParser , channelIds ,
312- item . getObject ( "gridVideoRenderer" ) );
355+ commitVideo (collector , timeAgoParser , item . getObject ( "gridVideoRenderer" ) ,
356+ channelVerifiedStatus , channelName , channelUrl );
313357 } else if (item .has ("gridPlaylistRenderer" )) {
314- getCommitPlaylistConsumer (collector , channelIds ,
315- item .getObject ("gridPlaylistRenderer" ));
358+ commitPlaylist (collector , item .getObject ("gridPlaylistRenderer" ),
359+ channelVerifiedStatus , channelName , channelUrl );
360+ } else if (item .has ("gridShowRenderer" )) {
361+ collector .commit (new YoutubeGridShowRendererChannelInfoItemExtractor (
362+ item .getObject ("gridShowRenderer" ), channelVerifiedStatus , channelName ,
363+ channelUrl ));
316364 } else if (item .has ("shelfRenderer" )) {
317365 return collectItem (collector , item .getObject ("shelfRenderer" )
318- .getObject ("content" ), channelIds );
366+ .getObject ("content" ), channelVerifiedStatus , channelName , channelUrl );
319367 } else if (item .has ("itemSectionRenderer" )) {
320368 return collectItemsFrom (collector , item .getObject ("itemSectionRenderer" )
321- .getArray ("contents" ), channelIds );
369+ .getArray ("contents" ), channelVerifiedStatus , channelName , channelUrl );
322370 } else if (item .has ("horizontalListRenderer" )) {
323371 return collectItemsFrom (collector , item .getObject ("horizontalListRenderer" )
324- .getArray ("items" ), channelIds );
372+ .getArray ("items" ), channelVerifiedStatus , channelName , channelUrl );
325373 } else if (item .has ("expandedShelfContentsRenderer" )) {
326374 return collectItemsFrom (collector , item .getObject ("expandedShelfContentsRenderer" )
327- .getArray ("items" ), channelIds );
375+ .getArray ("items" ), channelVerifiedStatus , channelName , channelUrl );
328376 } else if (item .has ("continuationItemRenderer" )) {
329377 return Optional .ofNullable (item .getObject ("continuationItemRenderer" ));
330378 }
331379
332380 return Optional .empty ();
333381 }
334382
335- private void getCommitVideoConsumer (@ Nonnull final MultiInfoItemsCollector collector ,
336- @ Nonnull final TimeAgoParser timeAgoParser ,
337- @ Nonnull final List <String > channelIds ,
338- @ Nonnull final JsonObject jsonObject ) {
383+ private static void commitReel (@ Nonnull final MultiInfoItemsCollector collector ,
384+ @ Nonnull final JsonObject reelItemRenderer ,
385+ @ Nonnull final VerifiedStatus channelVerifiedStatus ,
386+ @ Nullable final String channelName ,
387+ @ Nullable final String channelUrl ) {
339388 collector .commit (
340- new YoutubeStreamInfoItemExtractor ( jsonObject , timeAgoParser ) {
389+ new YoutubeReelInfoItemExtractor ( reelItemRenderer ) {
341390 @ Override
342391 public String getUploaderName () throws ParsingException {
343- if (channelIds .size () >= 2 ) {
344- return channelIds .get (0 );
345- }
346- return super .getUploaderName ();
392+ return isNullOrEmpty (channelName ) ? super .getUploaderName () : channelName ;
347393 }
348394
349395 @ Override
350396 public String getUploaderUrl () throws ParsingException {
351- if (channelIds .size () >= 2 ) {
352- return channelIds .get (1 );
353- }
354- return super .getUploaderUrl ();
397+ return isNullOrEmpty (channelUrl ) ? super .getUploaderName () : channelUrl ;
398+ }
399+
400+ @ Override
401+ public boolean isUploaderVerified () {
402+ return channelVerifiedStatus == VerifiedStatus .VERIFIED ;
355403 }
356404 });
357405 }
358406
359- private void getCommitReelItemConsumer (@ Nonnull final MultiInfoItemsCollector collector ,
360- @ Nonnull final List <String > channelIds ,
361- @ Nonnull final JsonObject jsonObject ) {
407+ private void commitVideo (@ Nonnull final MultiInfoItemsCollector collector ,
408+ @ Nonnull final TimeAgoParser timeAgoParser ,
409+ @ Nonnull final JsonObject jsonObject ,
410+ @ Nonnull final VerifiedStatus channelVerifiedStatus ,
411+ @ Nullable final String channelName ,
412+ @ Nullable final String channelUrl ) {
362413 collector .commit (
363- new YoutubeReelInfoItemExtractor (jsonObject ) {
414+ new YoutubeStreamInfoItemExtractor (jsonObject , timeAgoParser ) {
364415 @ Override
365416 public String getUploaderName () throws ParsingException {
366- if (channelIds .size () >= 2 ) {
367- return channelIds .get (0 );
368- }
369- return super .getUploaderName ();
417+ return isNullOrEmpty (channelName ) ? super .getUploaderName () : channelName ;
370418 }
371419
372420 @ Override
373421 public String getUploaderUrl () throws ParsingException {
374- if (channelIds .size () >= 2 ) {
375- return channelIds .get (1 );
422+ return isNullOrEmpty (channelUrl ) ? super .getUploaderName () : channelUrl ;
423+ }
424+
425+ @ SuppressWarnings ("DuplicatedCode" )
426+ @ Override
427+ public boolean isUploaderVerified () throws ParsingException {
428+ switch (channelVerifiedStatus ) {
429+ case VERIFIED :
430+ return true ;
431+ case UNVERIFIED :
432+ return false ;
433+ default :
434+ return super .isUploaderVerified ();
376435 }
377- return super .getUploaderUrl ();
378436 }
379437 });
380438 }
381439
382- private void getCommitPlaylistConsumer (@ Nonnull final MultiInfoItemsCollector collector ,
383- @ Nonnull final List <String > channelIds ,
384- @ Nonnull final JsonObject jsonObject ) {
440+ private void commitPlaylist (@ Nonnull final MultiInfoItemsCollector collector ,
441+ @ Nonnull final JsonObject jsonObject ,
442+ @ Nonnull final VerifiedStatus channelVerifiedStatus ,
443+ @ Nullable final String channelName ,
444+ @ Nullable final String channelUrl ) {
385445 collector .commit (
386446 new YoutubePlaylistInfoItemExtractor (jsonObject ) {
387447 @ Override
388448 public String getUploaderName () throws ParsingException {
389- if (channelIds .size () >= 2 ) {
390- return channelIds .get (0 );
391- }
392- return super .getUploaderName ();
449+ return isNullOrEmpty (channelName ) ? super .getUploaderName () : channelName ;
393450 }
394451
395452 @ Override
396453 public String getUploaderUrl () throws ParsingException {
397- if (channelIds .size () >= 2 ) {
398- return channelIds .get (1 );
454+ return isNullOrEmpty (channelUrl ) ? super .getUploaderName () : channelUrl ;
455+ }
456+
457+ @ SuppressWarnings ("DuplicatedCode" )
458+ @ Override
459+ public boolean isUploaderVerified () throws ParsingException {
460+ switch (channelVerifiedStatus ) {
461+ case VERIFIED :
462+ return true ;
463+ case UNVERIFIED :
464+ return false ;
465+ default :
466+ return super .isUploaderVerified ();
399467 }
400- return super .getUploaderUrl ();
401468 }
402469 });
403470 }
@@ -475,4 +542,59 @@ Optional<JsonObject> getTabData() {
475542 return Optional .of (tabRenderer );
476543 }
477544 }
545+
546+ /**
547+ * Enum representing the verified state of a channel
548+ */
549+ private enum VerifiedStatus {
550+ VERIFIED ,
551+ UNVERIFIED ,
552+ UNKNOWN
553+ }
554+
555+ private static final class YoutubeGridShowRendererChannelInfoItemExtractor
556+ extends YoutubeBaseShowInfoItemExtractor {
557+
558+ @ Nonnull
559+ private final VerifiedStatus verifiedStatus ;
560+
561+ @ Nullable
562+ private final String channelName ;
563+
564+ @ Nullable
565+ private final String channelUrl ;
566+
567+ private YoutubeGridShowRendererChannelInfoItemExtractor (
568+ @ Nonnull final JsonObject gridShowRenderer ,
569+ @ Nonnull final VerifiedStatus verifiedStatus ,
570+ @ Nullable final String channelName ,
571+ @ Nullable final String channelUrl ) {
572+ super (gridShowRenderer );
573+ this .verifiedStatus = verifiedStatus ;
574+ this .channelName = channelName ;
575+ this .channelUrl = channelUrl ;
576+ }
577+
578+ @ Override
579+ public String getUploaderName () {
580+ return channelName ;
581+ }
582+
583+ @ Override
584+ public String getUploaderUrl () {
585+ return channelUrl ;
586+ }
587+
588+ @ Override
589+ public boolean isUploaderVerified () throws ParsingException {
590+ switch (verifiedStatus ) {
591+ case VERIFIED :
592+ return true ;
593+ case UNVERIFIED :
594+ return false ;
595+ default :
596+ throw new ParsingException ("Could not get uploader verification status" );
597+ }
598+ }
599+ }
478600}
0 commit comments