Skip to content

Commit 5a6da5f

Browse files
committed
[YouTube] Support shows in channels and provide verified status to items
Also fix naming of info items' collection methods.
1 parent 9d5201f commit 5a6da5f

1 file changed

Lines changed: 181 additions & 59 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java

Lines changed: 181 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
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
*/
4444
public 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

Comments
 (0)