3636 * A {@link ChannelTabExtractor} implementation for the YouTube service.
3737 *
3838 * <p>
39- * It currently supports {@code Videos }, {@code Shorts }, {@code Live }, {@code Playlists },
40- * {@code Albums} and {@code Channels} tabs.
39+ * It currently supports {@code Featured }, {@code Videos }, {@code Shorts }, {@code Live },
40+ * {@code Playlists}, {@code Albums} and {@code Channels} tabs.
4141 * </p>
4242 */
4343public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@@ -48,6 +48,8 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
4848 private JsonObject jsonResponse ;
4949 private String channelId ;
5050
51+ private final String itemIndexKey = "itemIndex" ;
52+
5153 public YoutubeChannelTabExtractor (final StreamingService service ,
5254 final ListLinkHandler linkHandler ) {
5355 super (service , linkHandler );
@@ -57,6 +59,8 @@ public YoutubeChannelTabExtractor(final StreamingService service,
5759 private String getChannelTabsParameters () throws ParsingException {
5860 final String name = getName ();
5961 switch (name ) {
62+ case ChannelTabs .FEATURED :
63+ return "EghmZWF0dXJlZPIGBAoCMgA%3D" ;
6064 case ChannelTabs .VIDEOS :
6165 return "EgZ2aWRlb3PyBgQKAjoA" ;
6266 case ChannelTabs .SHORTS :
@@ -157,7 +161,7 @@ public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionEx
157161 final String channelName = getChannelName ();
158162 final String channelUrl = getUrl ();
159163
160- final JsonObject continuation = collectItemsFrom (collector , items , verifiedStatus ,
164+ final JsonObject continuation = collectItemsFrom (collector , - 1 , items , verifiedStatus ,
161165 channelName , channelUrl )
162166 .orElse (null );
163167
@@ -249,23 +253,38 @@ private Optional<JsonObject> collectItemsFrom(@Nonnull final MultiInfoItemsColle
249253 verifiedStatus = VerifiedStatus .UNKNOWN ;
250254 }
251255
252- return collectItemsFrom (collector , items , verifiedStatus , channelName , channelUrl );
256+ return collectItemsFrom (collector , - 1 , items , verifiedStatus , channelName , channelUrl );
253257 }
254258
255259 private Optional <JsonObject > collectItemsFrom (@ Nonnull final MultiInfoItemsCollector collector ,
260+ @ Nonnull final int rootItemIndex ,
256261 @ Nonnull final JsonArray items ,
257262 @ Nonnull final VerifiedStatus verifiedStatus ,
258263 @ Nullable final String channelName ,
259264 @ Nullable final String channelUrl ) {
265+
266+ // creating ItemIndex of the collectItemsFrom first call
267+ if (rootItemIndex == -1 ) {
268+ for (int i = 0 ; i < items .size (); i ++) {
269+ if (items .get (i ) instanceof JsonObject ) {
270+ ((JsonObject ) items .get (i )).put (itemIndexKey , i );
271+ }
272+ }
273+ }
274+
260275 return items .stream ()
261276 .filter (JsonObject .class ::isInstance )
262277 .map (JsonObject .class ::cast )
263- .map (item -> collectItem (
264- collector , item , verifiedStatus , channelName , channelUrl ))
278+ .map (item ->
279+ collectItem (collector ,
280+ (rootItemIndex == -1 ? item .getInt (itemIndexKey ) : rootItemIndex ),
281+ item , verifiedStatus , channelName , channelUrl )
282+ )
265283 .reduce (Optional .empty (), (c1 , c2 ) -> c1 .or (() -> c2 ));
266284 }
267285
268286 private Optional <JsonObject > collectItem (@ Nonnull final MultiInfoItemsCollector collector ,
287+ @ Nonnull final int rootItemIndex ,
269288 @ Nonnull final JsonObject item ,
270289 @ Nonnull final VerifiedStatus channelVerifiedStatus ,
271290 @ Nullable final String channelName ,
@@ -300,17 +319,35 @@ private Optional<JsonObject> collectItem(@Nonnull final MultiInfoItemsCollector
300319 item .getObject ("gridShowRenderer" ), channelVerifiedStatus , channelName ,
301320 channelUrl ));
302321 } else if (item .has ("shelfRenderer" )) {
303- return collectItem (collector , item .getObject ("shelfRenderer" )
304- .getObject ("content" ), channelVerifiedStatus , channelName , channelUrl );
322+ if (item .getObject ("shelfRenderer" ).getObject ("content" )
323+ .getObject ("horizontalListRenderer" ).has ("items" )) {
324+ commitRendererList (collector ,
325+ rootItemIndex , item .getObject ("shelfRenderer" ),
326+ channelVerifiedStatus , channelName , channelUrl );
327+ } else {
328+ return collectItem (collector , rootItemIndex , item .getObject ("shelfRenderer" )
329+ .getObject ("content" ), channelVerifiedStatus , channelName , channelUrl );
330+ }
331+ } else if (item .has ("channelVideoPlayerRenderer" )) {
332+ // Different InfoItem
333+ // skips until implemented
334+ return Optional .empty ();
335+ } else if (item .has ("recognitionShelfRenderer" )) {
336+ // Probably use RendererList with an extra extractor similar to FeaturedChannels
337+ // skips until implemented
338+ return Optional .empty ();
305339 } else if (item .has ("itemSectionRenderer" )) {
306- return collectItemsFrom (collector , item .getObject ("itemSectionRenderer" )
307- .getArray ("contents" ), channelVerifiedStatus , channelName , channelUrl );
340+ return collectItemsFrom (collector , rootItemIndex ,
341+ item .getObject ("itemSectionRenderer" ).getArray ("contents" ),
342+ channelVerifiedStatus , channelName , channelUrl );
308343 } else if (item .has ("horizontalListRenderer" )) {
309- return collectItemsFrom (collector , item .getObject ("horizontalListRenderer" )
310- .getArray ("items" ), channelVerifiedStatus , channelName , channelUrl );
344+ return collectItemsFrom (collector , rootItemIndex ,
345+ item .getObject ("horizontalListRenderer" ).getArray ("items" ),
346+ channelVerifiedStatus , channelName , channelUrl );
311347 } else if (item .has ("expandedShelfContentsRenderer" )) {
312- return collectItemsFrom (collector , item .getObject ("expandedShelfContentsRenderer" )
313- .getArray ("items" ), channelVerifiedStatus , channelName , channelUrl );
348+ return collectItemsFrom (collector , rootItemIndex ,
349+ item .getObject ("expandedShelfContentsRenderer" ).getArray ("items" ),
350+ channelVerifiedStatus , channelName , channelUrl );
314351 } else if (item .has ("lockupViewModel" )) {
315352 final JsonObject lockupViewModel = item .getObject ("lockupViewModel" );
316353 final String contentType = lockupViewModel .getString ("contentType" );
@@ -498,6 +535,52 @@ public boolean isUploaderVerified() throws ParsingException {
498535 });
499536 }
500537
538+
539+ private void commitRendererList (@ Nonnull final MultiInfoItemsCollector collector ,
540+ final int itemIndex ,
541+ @ Nonnull final JsonObject jsonObject ,
542+ @ Nonnull final VerifiedStatus channelVerifiedStatus ,
543+ @ Nullable final String channelName ,
544+ @ Nullable final String channelUrl
545+ ) {
546+
547+ String listItemsType = null ;
548+ if (jsonObject .getObject ("content" )
549+ .getObject ("horizontalListRenderer" ).getArray ("items" ).getObject (0 )
550+ .has ("gridChannelRenderer" )) {
551+ listItemsType = YoutubeShelfRendererListInfoItemExtractor .FEATURED_CHANNEL_LIST ;
552+ }
553+
554+ if (listItemsType != null ) {
555+ collector .commit (new YoutubeShelfRendererListInfoItemExtractor (jsonObject ,
556+ itemIndex , listItemsType ) {
557+ @ Override
558+ public String getUploaderName () throws ParsingException {
559+ return isNullOrEmpty (channelName ) ? super .getUploaderName () : channelName ;
560+ }
561+
562+ @ Override
563+ public String getUploaderUrl () throws ParsingException {
564+ return isNullOrEmpty (channelUrl ) ? super .getUploaderUrl () : channelUrl ;
565+ }
566+
567+ @ SuppressWarnings ("DuplicatedCode" )
568+ @ Override
569+ public boolean isUploaderVerified () throws ParsingException {
570+ switch (channelVerifiedStatus ) {
571+ case VERIFIED :
572+ return true ;
573+ case UNVERIFIED :
574+ return false ;
575+ default :
576+ return super .isUploaderVerified ();
577+ }
578+ }
579+ });
580+ }
581+ }
582+
583+
501584 @ Nullable
502585 private Page getNextPageFrom (final JsonObject continuations ,
503586 final List <String > channelIds ) throws IOException ,
0 commit comments