Skip to content

Commit c7e7404

Browse files
committed
feat: Featured Tab (Only Featured Channels RendererList supported)
1 parent 29c868e commit c7e7404

3 files changed

Lines changed: 221 additions & 14 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/MultiInfoItemsCollector.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
44
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
5+
import org.schabi.newpipe.extractor.channel.tabs.rendererlist.RendererListInfoItemExtractor;
6+
import org.schabi.newpipe.extractor.channel.tabs.rendererlist.RendererListInfoItemsCollector;
57
import org.schabi.newpipe.extractor.exceptions.ParsingException;
68
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
79
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
@@ -41,6 +43,7 @@
4143
* <li>{@link StreamInfoItemExtractor}</li>
4244
* <li>{@link ChannelInfoItemExtractor}</li>
4345
* <li>{@link PlaylistInfoItemExtractor}</li>
46+
* <li>{@link RendererListInfoItemExtractor}</li>
4447
* </ul>
4548
* Calling {@link #extract(InfoItemExtractor)} or {@link #commit(InfoItemExtractor)} with any
4649
* other extractor type will raise an exception.
@@ -49,12 +52,14 @@ public class MultiInfoItemsCollector extends InfoItemsCollector<InfoItem, InfoIt
4952
private final StreamInfoItemsCollector streamCollector;
5053
private final ChannelInfoItemsCollector userCollector;
5154
private final PlaylistInfoItemsCollector playlistCollector;
55+
private final RendererListInfoItemsCollector rendererListCollector;
5256

5357
public MultiInfoItemsCollector(final int serviceId) {
5458
super(serviceId);
5559
streamCollector = new StreamInfoItemsCollector(serviceId);
5660
userCollector = new ChannelInfoItemsCollector(serviceId);
5761
playlistCollector = new PlaylistInfoItemsCollector(serviceId);
62+
rendererListCollector = new RendererListInfoItemsCollector(serviceId);
5863
}
5964

6065
@Override
@@ -63,6 +68,7 @@ public List<Throwable> getErrors() {
6368
errors.addAll(streamCollector.getErrors());
6469
errors.addAll(userCollector.getErrors());
6570
errors.addAll(playlistCollector.getErrors());
71+
errors.addAll(rendererListCollector.getErrors());
6672

6773
return Collections.unmodifiableList(errors);
6874
}
@@ -73,6 +79,7 @@ public void reset() {
7379
streamCollector.reset();
7480
userCollector.reset();
7581
playlistCollector.reset();
82+
rendererListCollector.reset();
7683
}
7784

7885
@Override
@@ -84,6 +91,8 @@ public InfoItem extract(final InfoItemExtractor extractor) throws ParsingExcepti
8491
return userCollector.extract((ChannelInfoItemExtractor) extractor);
8592
} else if (extractor instanceof PlaylistInfoItemExtractor) {
8693
return playlistCollector.extract((PlaylistInfoItemExtractor) extractor);
94+
} else if (extractor instanceof RendererListInfoItemExtractor) {
95+
return rendererListCollector.extract((RendererListInfoItemExtractor) extractor);
8796
} else {
8897
throw new IllegalArgumentException("Invalid extractor type: " + extractor);
8998
}

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

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
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
*/
4343
public 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,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package org.schabi.newpipe.extractor.services.youtube.extractors;
2+
3+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
4+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromObject;
5+
6+
import com.grack.nanojson.JsonObject;
7+
8+
import org.schabi.newpipe.extractor.Image;
9+
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
10+
import org.schabi.newpipe.extractor.channel.tabs.rendererlist.RendererListInfoItemExtractor;
11+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
12+
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelTabLinkHandlerFactory;
13+
14+
import java.util.List;
15+
16+
import javax.annotation.Nonnull;
17+
18+
public class YoutubeShelfRendererListInfoItemExtractor implements RendererListInfoItemExtractor {
19+
20+
public static final String FEATURED_CHANNEL_LIST = "FEATURED_CHANNELS_LIST";
21+
private final JsonObject rendererListInfoItem;
22+
private final int parentListIndex;
23+
24+
private final String rendererListItemType;
25+
26+
public YoutubeShelfRendererListInfoItemExtractor(final JsonObject rendererListInfoItem,
27+
final int parentListIndex, final String rendererListItemType) {
28+
this.rendererListInfoItem = rendererListInfoItem;
29+
this.parentListIndex = parentListIndex;
30+
this.rendererListItemType = rendererListItemType;
31+
}
32+
33+
34+
@Override
35+
public String getName() throws ParsingException {
36+
try {
37+
final JsonObject title = this.rendererListInfoItem.getObject("title");
38+
String name = getTextFromObject(title);
39+
40+
if (name == null && this.rendererListInfoItem.isString("title")) {
41+
name = this.rendererListInfoItem.getString("title");
42+
}
43+
44+
return name;
45+
} catch (final Exception e) {
46+
throw new ParsingException("Could not get name", e);
47+
}
48+
}
49+
50+
@Override
51+
public String getUrl() throws ParsingException {
52+
try {
53+
final String url = getUrlFromObject(rendererListInfoItem.getObject("title"));
54+
55+
if (url == null) {
56+
final String uploaderTabURL = getUploaderTabUrl();
57+
58+
if (uploaderTabURL != null) {
59+
return uploaderTabURL + "/rendererlist/" + parentListIndex; // virtual url
60+
}
61+
}
62+
63+
return url;
64+
} catch (final Exception e) {
65+
throw new ParsingException("Could not get uploader url", e);
66+
}
67+
}
68+
69+
@Nonnull
70+
@Override
71+
public List<Image> getThumbnails() throws ParsingException {
72+
return List.of();
73+
}
74+
75+
@Override
76+
public int getParentListIndex() throws ParsingException {
77+
return parentListIndex;
78+
}
79+
80+
@Override
81+
public String getRendererListItemType() throws ParsingException {
82+
return this.rendererListItemType;
83+
}
84+
85+
@Override
86+
public String getUploaderTabUrl() throws ParsingException {
87+
try {
88+
final String uploaderUrl = getUploaderUrl();
89+
if (uploaderUrl != null) {
90+
return uploaderUrl
91+
+ YoutubeChannelTabLinkHandlerFactory
92+
.getUrlSuffix(ChannelTabs.FEATURED);
93+
}
94+
95+
return null;
96+
} catch (final Exception e) {
97+
throw new ParsingException("Could not get uploader url", e);
98+
}
99+
}
100+
101+
@Override
102+
public String getUploaderName() throws ParsingException {
103+
return null; // will not exist in rendererListInfoItem
104+
}
105+
106+
@Override
107+
public String getUploaderUrl() throws ParsingException {
108+
return null; // will not exist in rendererListInfoItem
109+
}
110+
111+
@Override
112+
public boolean isUploaderVerified() throws ParsingException {
113+
return false; // will not exist in rendererListInfoItem
114+
}
115+
}

0 commit comments

Comments
 (0)