Skip to content

Commit c181563

Browse files
committed
[YouTube] Implement support for collaborators
Implements support for extracting the collaborators of a video. This allows client apps to display them, if they wish so. The first collaborator is equivalent to the creator/uploader of the video. Ref: #1397
1 parent 59b620c commit c181563

12 files changed

Lines changed: 1503 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.schabi.newpipe.extractor;
2+
3+
import java.util.List;
4+
import javax.annotation.Nonnull;
5+
6+
/**
7+
* Class representing a creator of a piece of media that has been extracted.
8+
*/
9+
public final class Creator {
10+
11+
/**
12+
* Constant representing that the amount of subscribers of a {@link Creator} is unknown.
13+
*/
14+
public static final long UNKNOWN_SUBSCRIBER_COUNT = -1;
15+
16+
@Nonnull
17+
private final String name;
18+
@Nonnull
19+
private final String url;
20+
@Nonnull
21+
private final List<Image> avatars;
22+
private final long subscriberCount;
23+
private final boolean isVerified;
24+
25+
/**
26+
* Construct an {@link Creator} instance.
27+
*
28+
* @param name the name of the creator, which should be not null or empty
29+
* @param url the URL to the creator's page, which should be not null
30+
* or empty
31+
* @param avatars the avatar of the creator, possibly in multiple resolutions
32+
* @param subscriberCount the amount of subscribers/followers of the creator
33+
* @param isVerified whether the creator has been verified by the platform
34+
*/
35+
public Creator(@Nonnull final String name,
36+
@Nonnull final String url,
37+
@Nonnull final List<Image> avatars,
38+
final long subscriberCount,
39+
final boolean isVerified) {
40+
this.name = name;
41+
this.url = url;
42+
this.avatars = avatars;
43+
this.subscriberCount = subscriberCount;
44+
this.isVerified = isVerified;
45+
}
46+
47+
/**
48+
* Get the name of the {@link Creator}.
49+
*
50+
* @return the {@link Creator}'s name.
51+
*/
52+
@Nonnull
53+
public String getName() {
54+
return name;
55+
}
56+
57+
/**
58+
* Get the URL of the {@link Creator}.
59+
*
60+
* @return the {@link Creator}'s URL.
61+
*/
62+
@Nonnull
63+
public String getUrl() {
64+
return url;
65+
}
66+
67+
/**
68+
* Get the avatars of the {@link Creator}.
69+
*
70+
* @return the {@link Creator}'s avatars.
71+
*/
72+
@Nonnull
73+
public List<Image> getAvatars() {
74+
return avatars;
75+
}
76+
77+
/**
78+
* Get the amount of subscribers of this {@link Image}.
79+
*
80+
* <p>
81+
* If it is unknown, {@link #UNKNOWN_SUBSCRIBER_COUNT} is returned instead.
82+
* </p>
83+
*
84+
* @return the {@link Creator}'s amount of subscribers or {@link #UNKNOWN_SUBSCRIBER_COUNT}
85+
*/
86+
public long getSubscriberCount() {
87+
return subscriberCount;
88+
}
89+
90+
/**
91+
* Get whether the {@link Creator} has been verified by the platform.
92+
*
93+
* @return whether the {@link Creator} has been verified by the platform
94+
*/
95+
public boolean isVerified() {
96+
return isVerified;
97+
}
98+
99+
/**
100+
* Get a string representation of this {@link Creator} instance.
101+
*
102+
* @return a string representation of this {@link Creator} instance
103+
*/
104+
@Nonnull
105+
@Override
106+
public String toString() {
107+
return "Creator {" + "name=" + name + ", url=" + url + ", avatars=" + avatars
108+
+ ", subscriberCount=" + subscriberCount + ", isVerified=" + isVerified + "}";
109+
}
110+
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package org.schabi.newpipe.extractor.services.youtube;
2222

23+
import static org.schabi.newpipe.extractor.Creator.UNKNOWN_SUBSCRIBER_COUNT;
2324
import static org.schabi.newpipe.extractor.NewPipe.getDownloader;
2425
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_VERSION;
2526
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.DESKTOP_CLIENT_PLATFORM;
@@ -46,6 +47,7 @@
4647
import com.grack.nanojson.JsonWriter;
4748

4849
import org.jsoup.nodes.Entities;
50+
import org.schabi.newpipe.extractor.Creator;
4951
import org.schabi.newpipe.extractor.Image;
5052
import org.schabi.newpipe.extractor.Image.ResolutionLevel;
5153
import org.schabi.newpipe.extractor.downloader.Response;
@@ -1608,4 +1610,52 @@ public static JsonObject getFirstCollaborator(final JsonObject navigationEndpoin
16081610
return null;
16091611
}
16101612
}
1613+
1614+
/**
1615+
* Gets the first collaborator, which is the channel that owns the video,
1616+
* i.e. the video is displayed on their channel page.
1617+
*
1618+
* @param navigationEndpoint JSON object for the navigationEndpoint
1619+
* @return The first collaborator in the JSON object or {@code null}
1620+
*/
1621+
@Nullable
1622+
public static List<Creator> getCollaborators(final JsonObject navigationEndpoint)
1623+
throws ParsingException {
1624+
// CHECKSTYLE:OFF
1625+
final JsonArray listItems = JsonUtils.getArray(navigationEndpoint, "showDialogCommand.panelLoadingStrategy.inlineContent.dialogViewModel.customContent.listViewModel.listItems");
1626+
// CHECKSTYLE:ON
1627+
1628+
return listItems
1629+
.streamAsJsonObjects()
1630+
.map(item -> {
1631+
final JsonObject channel = item.getObject("listItemViewModel");
1632+
1633+
final String url = getUrlFromNavigationEndpoint(
1634+
channel.getObject("rendererContext")
1635+
.getObject("commandContext")
1636+
.getObject("onTap")
1637+
.getObject("innertubeCommand"));
1638+
final List<Image> avatars = getImagesFromThumbnailsArray(
1639+
channel.getObject("leadingAccessory")
1640+
.getObject("avatarViewModel")
1641+
.getObject("image")
1642+
.getArray("sources"));
1643+
1644+
long subscriberCount = UNKNOWN_SUBSCRIBER_COUNT;
1645+
try {
1646+
final String content = channel.getObject("subtitle").getString("content");
1647+
subscriberCount = Utils.mixedNumberWordToLong(content.split("•")[1]);
1648+
} catch (final NumberFormatException | ParsingException e) { }
1649+
1650+
return new Creator(
1651+
channel.getObject("title").getString("content"),
1652+
url,
1653+
avatars,
1654+
subscriberCount,
1655+
YoutubeParsingHelper.hasArtistOrVerifiedIconBadgeAttachment(
1656+
channel.getObject("title").getArray("attachmentRuns"))
1657+
);
1658+
})
1659+
.collect(Collectors.toList());
1660+
}
16111661
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.grack.nanojson.JsonObject;
4040
import com.grack.nanojson.JsonWriter;
4141

42+
import org.schabi.newpipe.extractor.Creator;
4243
import org.schabi.newpipe.extractor.Image;
4344
import org.schabi.newpipe.extractor.MediaFormat;
4445
import org.schabi.newpipe.extractor.MetaInfo;
@@ -1684,6 +1685,21 @@ public List<MetaInfo> getMetaInfo() throws ParsingException {
16841685
.getArray("contents"));
16851686
}
16861687

1688+
@Nonnull
1689+
@Override
1690+
public List<Creator> getCreators() throws ParsingException {
1691+
final JsonObject navigationEndpoint = JsonUtils.getObject(getVideoSecondaryInfoRenderer(),
1692+
"owner.videoOwnerRenderer.navigationEndpoint");
1693+
1694+
if (!navigationEndpoint.has("showDialogCommand")) {
1695+
// video has only one creator
1696+
return List.of(new Creator(getUploaderName(), getUploaderUrl(),
1697+
getUploaderAvatars(), getUploaderSubscriberCount(), isUploaderVerified()));
1698+
}
1699+
1700+
return YoutubeParsingHelper.getCollaborators(navigationEndpoint);
1701+
}
1702+
16871703
/**
16881704
* Set the {@link PoTokenProvider} instance to be used for fetching {@code poToken}s.
16891705
*

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package org.schabi.newpipe.extractor.services.youtube.extractors;
2020

21+
import static org.schabi.newpipe.extractor.Creator.UNKNOWN_SUBSCRIBER_COUNT;
2122
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
2223
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getThumbnailsFromInfoItem;
2324
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getImagesFromThumbnailsArray;
@@ -27,6 +28,7 @@
2728
import com.grack.nanojson.JsonArray;
2829
import com.grack.nanojson.JsonObject;
2930

31+
import org.schabi.newpipe.extractor.Creator;
3032
import org.schabi.newpipe.extractor.Image;
3133
import org.schabi.newpipe.extractor.exceptions.ParsingException;
3234
import org.schabi.newpipe.extractor.localization.DateWrapper;
@@ -502,4 +504,18 @@ public ContentAvailability getContentAvailability() throws ParsingException {
502504
return ContentAvailability.AVAILABLE;
503505
}
504506

507+
@Nonnull
508+
@Override
509+
public List<Creator> getCreators() throws ParsingException {
510+
final JsonObject navigationEndpoint = videoInfo.getObject("shortBylineText")
511+
.getArray("runs").getObject(0).getObject("navigationEndpoint");
512+
513+
if (!navigationEndpoint.has("showDialogCommand")) {
514+
// video has only one creator
515+
return List.of(new Creator(getUploaderName(), getUploaderUrl(),
516+
getUploaderAvatars(), UNKNOWN_SUBSCRIBER_COUNT, isUploaderVerified()));
517+
}
518+
519+
return YoutubeParsingHelper.getCollaborators(navigationEndpoint);
520+
}
505521
}

extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package org.schabi.newpipe.extractor.stream;
2222

23+
import org.schabi.newpipe.extractor.Creator;
2324
import org.schabi.newpipe.extractor.Image;
2425
import org.schabi.newpipe.extractor.InfoItem;
2526
import org.schabi.newpipe.extractor.InfoItemsCollector;
@@ -593,6 +594,16 @@ public ContentAvailability getContentAvailability() throws ParsingException {
593594
return ContentAvailability.UNKNOWN;
594595
}
595596

597+
/**
598+
* Gets the creators of the stream.
599+
*
600+
* @return The creators of the stream.
601+
*/
602+
@Nonnull
603+
public List<Creator> getCreators() throws ParsingException {
604+
return List.of();
605+
}
606+
596607
public enum Privacy {
597608
PUBLIC,
598609
UNLISTED,

extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package org.schabi.newpipe.extractor.stream;
2222

23+
import org.schabi.newpipe.extractor.Creator;
2324
import org.schabi.newpipe.extractor.Image;
2425
import org.schabi.newpipe.extractor.Info;
2526
import org.schabi.newpipe.extractor.InfoItem;
@@ -335,6 +336,11 @@ private static void extractOptionalData(final StreamInfo streamInfo,
335336
} catch (final Exception e) {
336337
streamInfo.addError(e);
337338
}
339+
try {
340+
streamInfo.setCreators(extractor.getCreators());
341+
} catch (final Exception e) {
342+
streamInfo.addError(e);
343+
}
338344

339345
streamInfo.setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo,
340346
extractor));
@@ -388,6 +394,7 @@ private static void extractOptionalData(final StreamInfo streamInfo,
388394
private boolean shortFormContent = false;
389395
@Nonnull
390396
private ContentAvailability contentAvailability = ContentAvailability.AVAILABLE;
397+
private List<Creator> creators = List.of();
391398

392399
/**
393400
* Preview frames, e.g. for the storyboard / seekbar thumbnail preview
@@ -743,4 +750,13 @@ public ContentAvailability getContentAvailability() {
743750
public void setContentAvailability(@Nonnull final ContentAvailability availability) {
744751
this.contentAvailability = availability;
745752
}
753+
754+
@Nonnull
755+
public List<Creator> getCreators() {
756+
return creators;
757+
}
758+
759+
public void setCreators(@Nonnull final List<Creator> creators) {
760+
this.creators = creators;
761+
}
746762
}

extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package org.schabi.newpipe.extractor.stream;
2222

23+
import org.schabi.newpipe.extractor.Creator;
2324
import org.schabi.newpipe.extractor.Image;
2425
import org.schabi.newpipe.extractor.InfoItem;
2526
import org.schabi.newpipe.extractor.localization.DateWrapper;
@@ -49,6 +50,8 @@ public class StreamInfoItem extends InfoItem {
4950
private boolean shortFormContent = false;
5051
@Nonnull
5152
private ContentAvailability contentAvailability = ContentAvailability.AVAILABLE;
53+
@Nonnull
54+
private List<Creator> creators = List.of();
5255

5356
public StreamInfoItem(final int serviceId,
5457
final String url,
@@ -162,6 +165,23 @@ public void setContentAvailability(@Nonnull final ContentAvailability availabili
162165
this.contentAvailability = availability;
163166
}
164167

168+
/**
169+
* Gets the creators of the stream.
170+
*
171+
* @return The creators of the stream.
172+
*/
173+
@Nonnull
174+
public List<Creator> getCreators() {
175+
return creators;
176+
}
177+
178+
/**
179+
* Sets the creators of the stream.
180+
*/
181+
public void setCreators(@Nonnull final List<Creator> creators) {
182+
this.creators = creators;
183+
}
184+
165185
@Override
166186
public String toString() {
167187
return "StreamInfoItem{"

extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemExtractor.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package org.schabi.newpipe.extractor.stream;
2222

23+
import org.schabi.newpipe.extractor.Creator;
2324
import org.schabi.newpipe.extractor.Image;
2425
import org.schabi.newpipe.extractor.InfoItemExtractor;
2526
import org.schabi.newpipe.extractor.exceptions.ParsingException;
@@ -162,4 +163,16 @@ default boolean isShortFormContent() throws ParsingException {
162163
default ContentAvailability getContentAvailability() throws ParsingException {
163164
return ContentAvailability.UNKNOWN;
164165
}
166+
167+
168+
/**
169+
* Get the creators/collaborators of the stream.
170+
*
171+
* @return The stream's creators
172+
* @throws ParsingException if there is an error in the extraction
173+
*/
174+
@Nonnull
175+
default List<Creator> getCreators() throws ParsingException {
176+
return List.of();
177+
}
165178
}

extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ public StreamInfoItem extract(final StreamInfoItemExtractor extractor) throws Pa
108108
} catch (final Exception e) {
109109
addError(e);
110110
}
111+
try {
112+
resultItem.setCreators(extractor.getCreators());
113+
} catch (final Exception e) {
114+
addError(e);
115+
}
111116

112117
return resultItem;
113118
}

0 commit comments

Comments
 (0)