2323import static org .schabi .newpipe .extractor .services .youtube .YoutubeChannelHelper .getChannelResponse ;
2424import static org .schabi .newpipe .extractor .services .youtube .YoutubeChannelHelper .resolveChannelId ;
2525import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getTextFromObject ;
26- import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
2726
2827import com .grack .nanojson .JsonArray ;
2928import com .grack .nanojson .JsonObject ;
5958
6059public class YoutubeChannelExtractor extends ChannelExtractor {
6160
61+ // Constants of objects used multiples from channel responses
62+ private static final String IMAGE = "image" ;
63+ private static final String CONTENTS = "contents" ;
64+ private static final String CONTENT_PREVIEW_IMAGE_VIEW_MODEL = "contentPreviewImageViewModel" ;
65+ private static final String PAGE_HEADER_VIEW_MODEL = "pageHeaderViewModel" ;
66+ private static final String TAB_RENDERER = "tabRenderer" ;
67+ private static final String CONTENT = "content" ;
68+ private static final String METADATA = "metadata" ;
69+ private static final String AVATAR = "avatar" ;
70+ private static final String THUMBNAILS = "thumbnails" ;
71+ private static final String SOURCES = "sources" ;
72+ private static final String BANNER = "banner" ;
73+
6274 private JsonObject jsonResponse ;
6375
6476 @ SuppressWarnings ("OptionalUsedAsFieldOrParameterType" )
@@ -95,28 +107,7 @@ public void onFetchPage(@Nonnull final Downloader downloader)
95107 jsonResponse = data .jsonResponse ;
96108 channelHeader = YoutubeChannelHelper .getChannelHeader (jsonResponse );
97109 channelId = data .channelId ;
98- channelAgeGateRenderer = getChannelAgeGateRenderer ();
99- }
100-
101- @ Nullable
102- private JsonObject getChannelAgeGateRenderer () {
103- return jsonResponse .getObject ("contents" )
104- .getObject ("twoColumnBrowseResultsRenderer" )
105- .getArray ("tabs" )
106- .stream ()
107- .filter (JsonObject .class ::isInstance )
108- .map (JsonObject .class ::cast )
109- .flatMap (tab -> tab .getObject ("tabRenderer" )
110- .getObject ("content" )
111- .getObject ("sectionListRenderer" )
112- .getArray ("contents" )
113- .stream ()
114- .filter (JsonObject .class ::isInstance )
115- .map (JsonObject .class ::cast ))
116- .filter (content -> content .has ("channelAgeGateRenderer" ))
117- .map (content -> content .getObject ("channelAgeGateRenderer" ))
118- .findFirst ()
119- .orElse (null );
110+ channelAgeGateRenderer = YoutubeChannelHelper .getChannelAgeGateRenderer (jsonResponse );
120111 }
121112
122113 @ Nonnull
@@ -133,94 +124,60 @@ public String getUrl() throws ParsingException {
133124 @ Override
134125 public String getId () throws ParsingException {
135126 assertPageFetched ();
136- return channelHeader .map (header -> header .json )
137- .flatMap (header -> Optional .ofNullable (header .getString ("channelId" ))
138- .or (() -> Optional .ofNullable (header .getObject ("navigationEndpoint" )
139- .getObject ("browseEndpoint" )
140- .getString ("browseId" ))
141- ))
142- .or (() -> Optional .ofNullable (channelId ))
143- .orElseThrow (() -> new ParsingException ("Could not get channel ID" ));
127+ return YoutubeChannelHelper .getChannelId (channelHeader , jsonResponse , channelId );
144128 }
145129
146130 @ Nonnull
147131 @ Override
148132 public String getName () throws ParsingException {
149133 assertPageFetched ();
150- if (channelAgeGateRenderer != null ) {
151- final String title = channelAgeGateRenderer .getString ("channelTitle" );
152- if (isNullOrEmpty (title )) {
153- throw new ParsingException ("Could not get channel name" );
154- }
155- return title ;
156- }
157-
158- final String metadataRendererTitle = jsonResponse .getObject ("metadata" )
159- .getObject ("channelMetadataRenderer" )
160- .getString ("title" );
161- if (!isNullOrEmpty (metadataRendererTitle )) {
162- return metadataRendererTitle ;
163- }
164-
165- return channelHeader .map (header -> {
166- final JsonObject channelJson = header .json ;
167- switch (header .headerType ) {
168- case PAGE :
169- return channelJson .getObject ("content" )
170- .getObject ("pageHeaderViewModel" )
171- .getObject ("title" )
172- .getObject ("dynamicTextViewModel" )
173- .getObject ("text" )
174- .getString ("content" , channelJson .getString ("pageTitle" ));
175-
176- case CAROUSEL :
177- case INTERACTIVE_TABBED :
178- return getTextFromObject (channelJson .getObject ("title" ));
179-
180- case C4_TABBED :
181- default :
182- return channelJson .getString ("title" );
183- }
184- })
185- // The channel name from a microformatDataRenderer may be different from the one displayed,
186- // especially for auto-generated channels, depending on the language requested for the
187- // interface (hl parameter of InnerTube requests' payload)
188- .or (() -> Optional .ofNullable (jsonResponse .getObject ("microformat" )
189- .getObject ("microformatDataRenderer" )
190- .getString ("title" )))
191- .orElseThrow (() -> new ParsingException ("Could not get channel name" ));
134+ return YoutubeChannelHelper .getChannelName (
135+ channelHeader , jsonResponse , channelAgeGateRenderer );
192136 }
193137
194138 @ Nonnull
195139 @ Override
196140 public List <Image > getAvatars () throws ParsingException {
197141 assertPageFetched ();
198142 if (channelAgeGateRenderer != null ) {
199- return Optional .ofNullable (channelAgeGateRenderer .getObject ("avatar" )
200- .getArray ("thumbnails" ))
143+ return Optional .ofNullable (channelAgeGateRenderer .getObject (AVATAR )
144+ .getArray (THUMBNAILS ))
201145 .map (YoutubeParsingHelper ::getImagesFromThumbnailsArray )
202146 .orElseThrow (() -> new ParsingException ("Could not get avatars" ));
203147 }
204148
205149 return channelHeader .map (header -> {
206150 switch (header .headerType ) {
207151 case PAGE :
208- return header .json .getObject ("content" )
209- .getObject ("pageHeaderViewModel" )
210- .getObject ("image" )
211- .getObject ("contentPreviewImageViewModel" )
212- .getObject ("image" )
213- .getArray ("sources" );
152+ final JsonObject imageObj = header .json .getObject (CONTENT )
153+ .getObject (PAGE_HEADER_VIEW_MODEL )
154+ .getObject (IMAGE );
155+
156+ if (imageObj .has (CONTENT_PREVIEW_IMAGE_VIEW_MODEL )) {
157+ return imageObj .getObject (CONTENT_PREVIEW_IMAGE_VIEW_MODEL )
158+ .getObject (IMAGE )
159+ .getArray (SOURCES );
160+ }
214161
162+ if (imageObj .has ("decoratedAvatarViewModel" )) {
163+ return imageObj .getObject ("decoratedAvatarViewModel" )
164+ .getObject (AVATAR )
165+ .getObject ("avatarViewModel" )
166+ .getObject (IMAGE )
167+ .getArray (SOURCES );
168+ }
169+
170+ // Return an empty avatar array as a fallback
171+ return new JsonArray ();
215172 case INTERACTIVE_TABBED :
216173 return header .json .getObject ("boxArt" )
217- .getArray ("thumbnails" );
174+ .getArray (THUMBNAILS );
218175
219176 case C4_TABBED :
220177 case CAROUSEL :
221178 default :
222- return header .json .getObject ("avatar" )
223- .getArray ("thumbnails" );
179+ return header .json .getObject (AVATAR )
180+ .getArray (THUMBNAILS );
224181 }
225182 })
226183 .map (YoutubeParsingHelper ::getImagesFromThumbnailsArray )
@@ -235,10 +192,27 @@ public List<Image> getBanners() {
235192 return List .of ();
236193 }
237194
238- // No banner is available on pageHeaderRenderer headers
239- return channelHeader .filter (header -> header .headerType != HeaderType .PAGE )
240- .map (header -> header .json .getObject ("banner" )
241- .getArray ("thumbnails" ))
195+ return channelHeader .map (header -> {
196+ if (header .headerType == HeaderType .PAGE ) {
197+ final JsonObject pageHeaderViewModel = header .json .getObject (CONTENT )
198+ .getObject (PAGE_HEADER_VIEW_MODEL );
199+
200+ if (pageHeaderViewModel .has (BANNER )) {
201+ return pageHeaderViewModel .getObject (BANNER )
202+ .getObject ("imageBannerViewModel" )
203+ .getObject (IMAGE )
204+ .getArray (SOURCES );
205+ }
206+
207+ // No banner is available (this should happen on pageHeaderRenderers of
208+ // system channels), use an empty JsonArray instead
209+ return new JsonArray ();
210+ }
211+
212+ return header .json
213+ .getObject (BANNER )
214+ .getArray (THUMBNAILS );
215+ })
242216 .map (YoutubeParsingHelper ::getImagesFromThumbnailsArray )
243217 .orElse (List .of ());
244218 }
@@ -264,14 +238,16 @@ public long getSubscriberCount() throws ParsingException {
264238 if (channelHeader .isPresent ()) {
265239 final ChannelHeader header = channelHeader .get ();
266240
267- if (header .headerType == HeaderType .INTERACTIVE_TABBED
268- || header .headerType == HeaderType .PAGE ) {
269- // No subscriber count is available on interactiveTabbedHeaderRenderer and
270- // pageHeaderRenderer headers
241+ if (header .headerType == HeaderType .INTERACTIVE_TABBED ) {
242+ // No subscriber count is available on interactiveTabbedHeaderRenderer header
271243 return UNKNOWN_SUBSCRIBER_COUNT ;
272244 }
273245
274246 final JsonObject headerJson = header .json ;
247+ if (header .headerType == HeaderType .PAGE ) {
248+ return getSubscriberCountFromPageChannelHeader (headerJson );
249+ }
250+
275251 JsonObject textObject = null ;
276252
277253 if (headerJson .has ("subscriberCountText" )) {
@@ -292,6 +268,51 @@ public long getSubscriberCount() throws ParsingException {
292268 return UNKNOWN_SUBSCRIBER_COUNT ;
293269 }
294270
271+ private long getSubscriberCountFromPageChannelHeader (@ Nonnull final JsonObject headerJson )
272+ throws ParsingException {
273+ final JsonObject metadataObject = headerJson .getObject (CONTENT )
274+ .getObject (PAGE_HEADER_VIEW_MODEL )
275+ .getObject (METADATA );
276+ if (metadataObject .has ("contentMetadataViewModel" )) {
277+ final JsonArray metadataPart = metadataObject .getObject ("contentMetadataViewModel" )
278+ .getArray ("metadataRows" )
279+ .stream ()
280+ .filter (JsonObject .class ::isInstance )
281+ .map (JsonObject .class ::cast )
282+ .map (metadataRow -> metadataRow .getArray ("metadataParts" ))
283+ /*
284+ Find metadata parts which have two elements: channel handle and subscriber
285+ count.
286+
287+ On autogenerated music channels, the subscriber count is not shown with this
288+ header.
289+
290+ Use the first metadata parts object found.
291+ */
292+ .filter (metadataParts -> metadataParts .size () == 2 )
293+ .findFirst ()
294+ .orElse (null );
295+ if (metadataPart == null ) {
296+ // As the parsing of the metadata parts object needed to get the subscriber count
297+ // is fragile, return UNKNOWN_SUBSCRIBER_COUNT when it cannot be got
298+ return UNKNOWN_SUBSCRIBER_COUNT ;
299+ }
300+
301+ try {
302+ // The subscriber count is at the same position for all languages as of 02/03/2024
303+ return Utils .mixedNumberWordToLong (metadataPart .getObject (0 )
304+ .getObject ("text" )
305+ .getString (CONTENT ));
306+ } catch (final NumberFormatException e ) {
307+ throw new ParsingException ("Could not get subscriber count" , e );
308+ }
309+ }
310+
311+ // If the channel header has no contentMetadataViewModel (which is the case for system
312+ // channels using this header), return UNKNOWN_SUBSCRIBER_COUNT
313+ return UNKNOWN_SUBSCRIBER_COUNT ;
314+ }
315+
295316 @ Override
296317 public String getDescription () throws ParsingException {
297318 assertPageFetched ();
@@ -302,12 +323,6 @@ public String getDescription() throws ParsingException {
302323 try {
303324 if (channelHeader .isPresent ()) {
304325 final ChannelHeader header = channelHeader .get ();
305-
306- if (header .headerType == HeaderType .PAGE ) {
307- // A pageHeaderRenderer doesn't contain a description
308- return null ;
309- }
310-
311326 if (header .headerType == HeaderType .INTERACTIVE_TABBED ) {
312327 /*
313328 In an interactiveTabbedHeaderRenderer, the real description, is only available
@@ -322,7 +337,7 @@ public String getDescription() throws ParsingException {
322337 }
323338
324339 // The description is cut and the original one can be only accessed from the About tab
325- return jsonResponse .getObject ("metadata " )
340+ return jsonResponse .getObject ("title " )
326341 .getObject ("channelMetadataRenderer" )
327342 .getString ("description" );
328343 } catch (final Exception e ) {
@@ -371,7 +386,7 @@ public List<ListLinkHandler> getTabs() throws ParsingException {
371386
372387 @ Nonnull
373388 private List <ListLinkHandler > getTabsForNonAgeRestrictedChannels () throws ParsingException {
374- final JsonArray responseTabs = jsonResponse .getObject ("contents" )
389+ final JsonArray responseTabs = jsonResponse .getObject (CONTENTS )
375390 .getObject ("twoColumnBrowseResultsRenderer" )
376391 .getArray ("tabs" );
377392
@@ -392,8 +407,8 @@ private List<ListLinkHandler> getTabsForNonAgeRestrictedChannels() throws Parsin
392407 responseTabs .stream ()
393408 .filter (JsonObject .class ::isInstance )
394409 .map (JsonObject .class ::cast )
395- .filter (tab -> tab .has ("tabRenderer" ))
396- .map (tab -> tab .getObject ("tabRenderer" ))
410+ .filter (tab -> tab .has (TAB_RENDERER ))
411+ .map (tab -> tab .getObject (TAB_RENDERER ))
397412 .forEach (tabRenderer -> {
398413 final String tabUrl = tabRenderer .getObject ("endpoint" )
399414 .getObject ("commandMetadata" )
@@ -432,6 +447,9 @@ private List<ListLinkHandler> getTabsForNonAgeRestrictedChannels() throws Parsin
432447 case "playlists" :
433448 addNonVideosTab .accept (ChannelTabs .PLAYLISTS );
434449 break ;
450+ default :
451+ // Unsupported channel tab, ignore it
452+ break ;
435453 }
436454 }
437455 });
0 commit comments