Skip to content

Commit 42c1afa

Browse files
committed
[YouTube] Fix serialization of Videos channel tab when already fetched
Also remove usage of Optional as fields as it is not a good practice. This simplifies in some places channel info extraction code.
1 parent 596bce2 commit 42c1afa

3 files changed

Lines changed: 114 additions & 96 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelHelper.java

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import javax.annotation.Nonnull;
1212
import javax.annotation.Nullable;
1313
import java.io.IOException;
14+
import java.io.Serializable;
1415
import java.nio.charset.StandardCharsets;
1516
import java.util.Optional;
1617

@@ -233,7 +234,7 @@ private static void checkIfChannelResponseIsValid(@Nonnull final JsonObject json
233234
* properties.
234235
* </p>
235236
*/
236-
public static final class ChannelHeader {
237+
public static final class ChannelHeader implements Serializable {
237238

238239
/**
239240
* Types of supported YouTube channel headers.
@@ -294,27 +295,27 @@ public enum HeaderType {
294295
*/
295296
public final HeaderType headerType;
296297

297-
private ChannelHeader(@Nonnull final JsonObject json, final HeaderType headerType) {
298+
public ChannelHeader(@Nonnull final JsonObject json, final HeaderType headerType) {
298299
this.json = json;
299300
this.headerType = headerType;
300301
}
301302
}
302303

303304
/**
304-
* Get a channel header as an {@link Optional} it if exists.
305+
* Get a channel header it if exists.
305306
*
306307
* @param channelResponse a full channel JSON response
307-
* @return an {@link Optional} containing a {@link ChannelHeader} or an empty {@link Optional}
308-
* if no supported header has been found
308+
* @return a {@link ChannelHeader} or {@code null} if no supported header has been found
309309
*/
310-
@Nonnull
311-
public static Optional<ChannelHeader> getChannelHeader(
310+
@Nullable
311+
public static ChannelHeader getChannelHeader(
312312
@Nonnull final JsonObject channelResponse) {
313313
final JsonObject header = channelResponse.getObject(HEADER);
314314

315315
if (header.has(C4_TABBED_HEADER_RENDERER)) {
316316
return Optional.of(header.getObject(C4_TABBED_HEADER_RENDERER))
317-
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.C4_TABBED));
317+
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.C4_TABBED))
318+
.orElse(null);
318319
} else if (header.has(CAROUSEL_HEADER_RENDERER)) {
319320
return header.getObject(CAROUSEL_HEADER_RENDERER)
320321
.getArray(CONTENTS)
@@ -324,17 +325,20 @@ public static Optional<ChannelHeader> getChannelHeader(
324325
.filter(item -> item.has(TOPIC_CHANNEL_DETAILS_RENDERER))
325326
.findFirst()
326327
.map(item -> item.getObject(TOPIC_CHANNEL_DETAILS_RENDERER))
327-
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.CAROUSEL));
328+
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.CAROUSEL))
329+
.orElse(null);
328330
} else if (header.has("pageHeaderRenderer")) {
329331
return Optional.of(header.getObject("pageHeaderRenderer"))
330-
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.PAGE));
332+
.map(json -> new ChannelHeader(json, ChannelHeader.HeaderType.PAGE))
333+
.orElse(null);
331334
} else if (header.has("interactiveTabbedHeaderRenderer")) {
332335
return Optional.of(header.getObject("interactiveTabbedHeaderRenderer"))
333336
.map(json -> new ChannelHeader(json,
334-
ChannelHeader.HeaderType.INTERACTIVE_TABBED));
335-
} else {
336-
return Optional.empty();
337+
ChannelHeader.HeaderType.INTERACTIVE_TABBED))
338+
.orElse(null);
337339
}
340+
341+
return null;
338342
}
339343

340344
/**
@@ -418,20 +422,18 @@ public static boolean isChannelVerified(@Nonnull final ChannelHeader channelHead
418422
* If the ID cannot still be get, the fallback channel ID, if provided, will be used.
419423
* </p>
420424
*
421-
* @param header the channel header
425+
* @param channelHeader the channel header
422426
* @param fallbackChannelId the fallback channel ID, which can be null
423427
* @return the ID of the channel
424428
* @throws ParsingException if the channel ID cannot be got from the channel header, the
425429
* channel response and the fallback channel ID
426430
*/
427431
@Nonnull
428432
public static String getChannelId(
429-
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
430-
@Nonnull final Optional<ChannelHeader> header,
433+
@Nullable final ChannelHeader channelHeader,
431434
@Nonnull final JsonObject jsonResponse,
432435
@Nullable final String fallbackChannelId) throws ParsingException {
433-
if (header.isPresent()) {
434-
final ChannelHeader channelHeader = header.get();
436+
if (channelHeader != null) {
435437
switch (channelHeader.headerType) {
436438
case C4_TABBED:
437439
final String channelId = channelHeader.json.getObject(HEADER)
@@ -486,10 +488,9 @@ public static String getChannelId(
486488
}
487489

488490
@Nonnull
489-
public static String getChannelName(@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
490-
@Nonnull final Optional<ChannelHeader> channelHeader,
491-
@Nonnull final JsonObject jsonResponse,
492-
@Nullable final JsonObject channelAgeGateRenderer)
491+
public static String getChannelName(@Nullable final ChannelHeader channelHeader,
492+
@Nullable final JsonObject channelAgeGateRenderer,
493+
@Nonnull final JsonObject jsonResponse)
493494
throws ParsingException {
494495
if (channelAgeGateRenderer != null) {
495496
final String title = channelAgeGateRenderer.getString("channelTitle");
@@ -506,7 +507,8 @@ public static String getChannelName(@SuppressWarnings("OptionalUsedAsFieldOrPara
506507
return metadataRendererTitle;
507508
}
508509

509-
return channelHeader.map(header -> {
510+
return Optional.ofNullable(channelHeader)
511+
.map(header -> {
510512
final JsonObject channelJson = header.json;
511513
switch (header.headerType) {
512514
case PAGE:

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

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
7373

7474
private JsonObject jsonResponse;
7575

76-
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
77-
private Optional<ChannelHeader> channelHeader;
76+
@Nullable
77+
private ChannelHeader channelHeader;
7878

7979
private String channelId;
8080

@@ -132,7 +132,7 @@ public String getId() throws ParsingException {
132132
public String getName() throws ParsingException {
133133
assertPageFetched();
134134
return YoutubeChannelHelper.getChannelName(
135-
channelHeader, jsonResponse, channelAgeGateRenderer);
135+
channelHeader, channelAgeGateRenderer, jsonResponse);
136136
}
137137

138138
@Nonnull
@@ -146,40 +146,40 @@ public List<Image> getAvatars() throws ParsingException {
146146
.orElseThrow(() -> new ParsingException("Could not get avatars"));
147147
}
148148

149-
return channelHeader.map(header -> {
150-
switch (header.headerType) {
151-
case PAGE:
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-
}
161-
162-
if (imageObj.has("decoratedAvatarViewModel")) {
163-
return imageObj.getObject("decoratedAvatarViewModel")
164-
.getObject(AVATAR)
165-
.getObject("avatarViewModel")
166-
.getObject(IMAGE)
167-
.getArray(SOURCES);
149+
return Optional.ofNullable(channelHeader)
150+
.map(header -> {
151+
switch (header.headerType) {
152+
case PAGE:
153+
final JsonObject imageObj = header.json.getObject(CONTENT)
154+
.getObject(PAGE_HEADER_VIEW_MODEL)
155+
.getObject(IMAGE);
156+
157+
if (imageObj.has(CONTENT_PREVIEW_IMAGE_VIEW_MODEL)) {
158+
return imageObj.getObject(CONTENT_PREVIEW_IMAGE_VIEW_MODEL)
159+
.getObject(IMAGE)
160+
.getArray(SOURCES);
161+
}
162+
163+
if (imageObj.has("decoratedAvatarViewModel")) {
164+
return imageObj.getObject("decoratedAvatarViewModel")
165+
.getObject(AVATAR)
166+
.getObject("avatarViewModel")
167+
.getObject(IMAGE)
168+
.getArray(SOURCES);
169+
}
170+
171+
// Return an empty avatar array as a fallback
172+
return new JsonArray();
173+
case INTERACTIVE_TABBED:
174+
return header.json.getObject("boxArt")
175+
.getArray(THUMBNAILS);
176+
case C4_TABBED:
177+
case CAROUSEL:
178+
default:
179+
return header.json.getObject(AVATAR)
180+
.getArray(THUMBNAILS);
168181
}
169-
170-
// Return an empty avatar array as a fallback
171-
return new JsonArray();
172-
case INTERACTIVE_TABBED:
173-
return header.json.getObject("boxArt")
174-
.getArray(THUMBNAILS);
175-
176-
case C4_TABBED:
177-
case CAROUSEL:
178-
default:
179-
return header.json.getObject(AVATAR)
180-
.getArray(THUMBNAILS);
181-
}
182-
})
182+
})
183183
.map(YoutubeParsingHelper::getImagesFromThumbnailsArray)
184184
.orElseThrow(() -> new ParsingException("Could not get avatars"));
185185
}
@@ -192,7 +192,8 @@ public List<Image> getBanners() {
192192
return List.of();
193193
}
194194

195-
return channelHeader.map(header -> {
195+
return Optional.ofNullable(channelHeader)
196+
.map(header -> {
196197
if (header.headerType == HeaderType.PAGE) {
197198
final JsonObject pageHeaderViewModel = header.json.getObject(CONTENT)
198199
.getObject(PAGE_HEADER_VIEW_MODEL);
@@ -235,16 +236,14 @@ public long getSubscriberCount() throws ParsingException {
235236
return UNKNOWN_SUBSCRIBER_COUNT;
236237
}
237238

238-
if (channelHeader.isPresent()) {
239-
final ChannelHeader header = channelHeader.get();
240-
241-
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
239+
if (channelHeader != null) {
240+
if (channelHeader.headerType == HeaderType.INTERACTIVE_TABBED) {
242241
// No subscriber count is available on interactiveTabbedHeaderRenderer header
243242
return UNKNOWN_SUBSCRIBER_COUNT;
244243
}
245244

246-
final JsonObject headerJson = header.json;
247-
if (header.headerType == HeaderType.PAGE) {
245+
final JsonObject headerJson = channelHeader.json;
246+
if (channelHeader.headerType == HeaderType.PAGE) {
248247
return getSubscriberCountFromPageChannelHeader(headerJson);
249248
}
250249

@@ -321,19 +320,17 @@ public String getDescription() throws ParsingException {
321320
}
322321

323322
try {
324-
if (channelHeader.isPresent()) {
325-
final ChannelHeader header = channelHeader.get();
326-
if (header.headerType == HeaderType.INTERACTIVE_TABBED) {
327-
/*
328-
In an interactiveTabbedHeaderRenderer, the real description, is only available
329-
in its header
330-
The other one returned in non-About tabs accessible in the
331-
microformatDataRenderer object of the response may be completely different
332-
The description extracted is incomplete and the original one can be only
333-
accessed from the About tab
334-
*/
335-
return getTextFromObject(header.json.getObject("description"));
336-
}
323+
if (channelHeader != null
324+
&& channelHeader.headerType == HeaderType.INTERACTIVE_TABBED) {
325+
/*
326+
In an interactiveTabbedHeaderRenderer, the real description, is only available
327+
in its header
328+
The other one returned in non-About tabs accessible in the
329+
microformatDataRenderer object of the response may be completely different
330+
The description extracted is incomplete and the original one can be only
331+
accessed from the About tab
332+
*/
333+
return getTextFromObject(channelHeader.json.getObject("description"));
337334
}
338335

339336
return jsonResponse.getObject(METADATA)
@@ -368,8 +365,12 @@ public boolean isVerified() throws ParsingException {
368365
return false;
369366
}
370367

371-
return YoutubeChannelHelper.isChannelVerified(channelHeader.orElseThrow(() ->
372-
new ParsingException("Could not get verified status")));
368+
if (channelHeader == null) {
369+
throw new ParsingException(
370+
"Could not get channel verified status, no channel header has been extracted");
371+
}
372+
373+
return YoutubeChannelHelper.isChannelVerified(channelHeader);
373374
}
374375

375376
@Nonnull
@@ -421,6 +422,19 @@ private List<ListLinkHandler> getTabsForNonAgeRestrictedChannels() throws Parsin
421422

422423
final String urlSuffix = urlParts[urlParts.length - 1];
423424

425+
/*
426+
Make a copy of the channelHeader member to avoid keeping a reference to
427+
this YoutubeChannelExtractor instance which would prevent serialization of
428+
the ReadyChannelTabListLinkHandler instance created above
429+
*/
430+
final ChannelHeader channelHeaderCopy;
431+
if (channelHeader == null) {
432+
channelHeaderCopy = null;
433+
} else {
434+
channelHeaderCopy = new ChannelHeader(channelHeader.json,
435+
channelHeader.headerType);
436+
}
437+
424438
switch (urlSuffix) {
425439
case "videos":
426440
// Since the Videos tab has already its contents fetched, make
@@ -431,9 +445,8 @@ private List<ListLinkHandler> getTabsForNonAgeRestrictedChannels() throws Parsin
431445
channelId,
432446
ChannelTabs.VIDEOS,
433447
(service, linkHandler) -> new VideosTabExtractor(
434-
service, linkHandler, tabRenderer, channelHeader,
435-
name, id, url)));
436-
448+
service, linkHandler, tabRenderer,
449+
channelHeaderCopy, name, id, url)));
437450
break;
438451
case "shorts":
439452
addNonVideosTab.accept(ChannelTabs.SHORTS);

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@
4242
*/
4343
public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
4444

45+
@Nullable
46+
protected YoutubeChannelHelper.ChannelHeader channelHeader;
47+
4548
private JsonObject jsonResponse;
4649
private String channelId;
47-
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
48-
protected Optional<YoutubeChannelHelper.ChannelHeader> channelHeader;
4950

5051
public YoutubeChannelTabExtractor(final StreamingService service,
5152
final ListLinkHandler linkHandler) {
@@ -104,9 +105,9 @@ public String getId() throws ParsingException {
104105
}
105106

106107
protected String getChannelName() throws ParsingException {
107-
return YoutubeChannelHelper.getChannelName(
108-
channelHeader, jsonResponse,
109-
YoutubeChannelHelper.getChannelAgeGateRenderer(jsonResponse));
108+
return YoutubeChannelHelper.getChannelName(channelHeader,
109+
YoutubeChannelHelper.getChannelAgeGateRenderer(jsonResponse),
110+
jsonResponse);
110111
}
111112

112113
@Nonnull
@@ -140,11 +141,14 @@ public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionEx
140141
}
141142
}
142143

143-
final VerifiedStatus verifiedStatus = channelHeader.flatMap(header ->
144-
YoutubeChannelHelper.isChannelVerified(header)
145-
? Optional.of(VerifiedStatus.VERIFIED)
146-
: Optional.of(VerifiedStatus.UNVERIFIED))
147-
.orElse(VerifiedStatus.UNKNOWN);
144+
final VerifiedStatus verifiedStatus;
145+
if (channelHeader == null) {
146+
verifiedStatus = VerifiedStatus.UNKNOWN;
147+
} else {
148+
verifiedStatus = YoutubeChannelHelper.isChannelVerified(channelHeader)
149+
? VerifiedStatus.VERIFIED
150+
: VerifiedStatus.UNVERIFIED;
151+
}
148152

149153
// If a channel tab is fetched, the next page requires channel ID and name, as channel
150154
// streams don't have their channel specified.
@@ -462,8 +466,7 @@ public static final class VideosTabExtractor extends YoutubeChannelTabExtractor
462466
VideosTabExtractor(final StreamingService service,
463467
final ListLinkHandler linkHandler,
464468
final JsonObject tabRenderer,
465-
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
466-
final Optional<YoutubeChannelHelper.ChannelHeader> channelHeader,
469+
@Nullable final YoutubeChannelHelper.ChannelHeader channelHeader,
467470
final String channelName,
468471
final String channelId,
469472
final String channelUrl) {

0 commit comments

Comments
 (0)