|
20 | 20 |
|
21 | 21 | package org.schabi.newpipe.extractor.services.youtube; |
22 | 22 |
|
23 | | -import static org.schabi.newpipe.extractor.NewPipe.getDownloader; |
24 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_VERSION; |
25 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.DESKTOP_CLIENT_PLATFORM; |
26 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_CLIENT_VERSION; |
27 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_DEVICE_MODEL; |
28 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_USER_AGENT_VERSION; |
29 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_USER_AGENT; |
30 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_ID; |
31 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_NAME; |
32 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_HARDCODED_CLIENT_VERSION; |
33 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_CLIENT_ID; |
34 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_CLIENT_NAME; |
35 | | -import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_HARDCODED_CLIENT_VERSION; |
36 | | -import static org.schabi.newpipe.extractor.utils.Utils.HTTP; |
37 | | -import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; |
38 | | -import static org.schabi.newpipe.extractor.utils.Utils.getStringResultFromRegexArray; |
39 | | -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; |
40 | | - |
41 | 23 | import com.grack.nanojson.JsonArray; |
42 | 24 | import com.grack.nanojson.JsonBuilder; |
43 | 25 | import com.grack.nanojson.JsonObject; |
44 | 26 | import com.grack.nanojson.JsonParser; |
45 | 27 | import com.grack.nanojson.JsonParserException; |
46 | 28 | import com.grack.nanojson.JsonWriter; |
47 | | - |
48 | 29 | import org.jsoup.nodes.Entities; |
49 | 30 | import org.schabi.newpipe.extractor.Image; |
50 | 31 | import org.schabi.newpipe.extractor.Image.ResolutionLevel; |
|
63 | 44 | import org.schabi.newpipe.extractor.utils.RandomStringFromAlphabetGenerator; |
64 | 45 | import org.schabi.newpipe.extractor.utils.Utils; |
65 | 46 |
|
| 47 | +import javax.annotation.Nonnull; |
| 48 | +import javax.annotation.Nullable; |
66 | 49 | import java.io.IOException; |
67 | 50 | import java.net.MalformedURLException; |
68 | 51 | import java.net.URL; |
|
79 | 62 | import java.util.stream.Collectors; |
80 | 63 | import java.util.stream.Stream; |
81 | 64 |
|
82 | | -import javax.annotation.Nonnull; |
83 | | -import javax.annotation.Nullable; |
| 65 | +import static org.schabi.newpipe.extractor.NewPipe.getDownloader; |
| 66 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_VERSION; |
| 67 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.DESKTOP_CLIENT_PLATFORM; |
| 68 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_CLIENT_VERSION; |
| 69 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_DEVICE_MODEL; |
| 70 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_USER_AGENT_VERSION; |
| 71 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_USER_AGENT; |
| 72 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_ID; |
| 73 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_NAME; |
| 74 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_HARDCODED_CLIENT_VERSION; |
| 75 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_CLIENT_ID; |
| 76 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_CLIENT_NAME; |
| 77 | +import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_HARDCODED_CLIENT_VERSION; |
| 78 | +import static org.schabi.newpipe.extractor.utils.Utils.HTTP; |
| 79 | +import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; |
| 80 | +import static org.schabi.newpipe.extractor.utils.Utils.getStringResultFromRegexArray; |
| 81 | +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; |
84 | 82 |
|
85 | 83 | public final class YoutubeParsingHelper { |
86 | 84 |
|
@@ -198,7 +196,7 @@ private YoutubeParsingHelper() { |
198 | 196 |
|
199 | 197 | private static boolean consentAccepted = false; |
200 | 198 |
|
201 | | - private static final Predicate<String> STRING_PREDICATE = text -> !text.isEmpty(); |
| 199 | + public static final Predicate<String> STRING_PREDICATE = text -> !text.isBlank(); |
202 | 200 |
|
203 | 201 | public static boolean isGoogleURL(final String url) { |
204 | 202 | final String cachedUrl = extractCachedUrlIfNeeded(url); |
@@ -757,10 +755,18 @@ public static Optional<String> getUrlFromNavigationEndpoint( |
757 | 755 | .map(id -> "https://www.youtube.com/playlist?list=" + id); |
758 | 756 | }) |
759 | 757 | .or(() -> { |
760 | | - final var metadata = navigationEndpoint.getObject("commandMetadata") |
761 | | - .getObject("webCommandMetadata"); |
762 | | - return Optional.ofNullable(metadata.getString("url")) |
763 | | - .map(url -> "https://www.youtube.com" + url); |
| 758 | + final var listItems = navigationEndpoint.getObject("showDialogCommand") |
| 759 | + .getObject("panelLoadingStrategy").getObject("inlineContent") |
| 760 | + .getObject("dialogViewModel").getObject("customContent") |
| 761 | + .getObject("listViewModel") |
| 762 | + .getArray("listItems"); |
| 763 | + |
| 764 | + // the first item seems to always be the channel that actually uploaded the |
| 765 | + // video, i.e. it appears in their video feed |
| 766 | + final var command = listItems.getObject(0).getObject("listItemViewModel") |
| 767 | + .getObject("rendererContext").getObject("commandContext") |
| 768 | + .getObject("onTap").getObject("innertubeCommand"); |
| 769 | + return getUrlFromNavigationEndpoint(command); |
764 | 770 | }) |
765 | 771 | .filter(STRING_PREDICATE); |
766 | 772 | } |
@@ -1248,36 +1254,25 @@ public static String extractCachedUrlIfNeeded(final String url) { |
1248 | 1254 | return url; |
1249 | 1255 | } |
1250 | 1256 |
|
1251 | | - public static boolean isVerified(final JsonArray badges) { |
1252 | | - if (Utils.isNullOrEmpty(badges)) { |
1253 | | - return false; |
1254 | | - } |
1255 | | - |
1256 | | - for (final Object badge : badges) { |
1257 | | - final String style = ((JsonObject) badge).getObject("metadataBadgeRenderer") |
1258 | | - .getString("style"); |
1259 | | - if (style != null && (style.equals("BADGE_STYLE_TYPE_VERIFIED") |
1260 | | - || style.equals("BADGE_STYLE_TYPE_VERIFIED_ARTIST"))) { |
1261 | | - return true; |
1262 | | - } |
1263 | | - } |
1264 | | - |
1265 | | - return false; |
| 1257 | + public static boolean isVerified(@Nonnull final JsonArray badges) { |
| 1258 | + return badges.streamAsJsonObjects() |
| 1259 | + .anyMatch(badge -> { |
| 1260 | + final String style = badge.getObject("metadataBadgeRenderer") |
| 1261 | + .getString("style"); |
| 1262 | + return "BADGE_STYLE_TYPE_VERIFIED".equals(style) |
| 1263 | + || "BADGE_STYLE_TYPE_VERIFIED_ARTIST".equals(style); |
| 1264 | + }); |
1266 | 1265 | } |
1267 | 1266 |
|
1268 | 1267 | public static boolean hasArtistOrVerifiedIconBadgeAttachment( |
1269 | 1268 | @Nonnull final JsonArray attachmentRuns) { |
1270 | | - return attachmentRuns.stream() |
1271 | | - .filter(JsonObject.class::isInstance) |
1272 | | - .map(JsonObject.class::cast) |
| 1269 | + return attachmentRuns.streamAsJsonObjects() |
1273 | 1270 | .anyMatch(attachmentRun -> attachmentRun.getObject("element") |
1274 | 1271 | .getObject("type") |
1275 | 1272 | .getObject("imageType") |
1276 | 1273 | .getObject("image") |
1277 | 1274 | .getArray("sources") |
1278 | | - .stream() |
1279 | | - .filter(JsonObject.class::isInstance) |
1280 | | - .map(JsonObject.class::cast) |
| 1275 | + .streamAsJsonObjects() |
1281 | 1276 | .anyMatch(source -> { |
1282 | 1277 | final String imageName = source.getObject("clientResource") |
1283 | 1278 | .getString("imageName"); |
@@ -1541,4 +1536,22 @@ public static JsonBuilder<JsonObject> prepareJsonBuilder( |
1541 | 1536 |
|
1542 | 1537 | return builder; |
1543 | 1538 | } |
| 1539 | + |
| 1540 | + /** |
| 1541 | + * Gets the first collaborator, which is the channel that owns the video, |
| 1542 | + * i.e. the video is displayed on their channel page. |
| 1543 | + * |
| 1544 | + * @param renderer JSON object for the video renderer |
| 1545 | + * @return An {@link Optional} containing the first collaborator, if one is present |
| 1546 | + */ |
| 1547 | + @Nonnull |
| 1548 | + public static Optional<JsonObject> getFirstCollaborator(final JsonObject renderer) { |
| 1549 | + final JsonArray listItems = renderer.getObject("navigationEndpoint") |
| 1550 | + .getObject("showDialogCommand").getObject("panelLoadingStrategy") |
| 1551 | + .getObject("inlineContent").getObject("dialogViewModel") |
| 1552 | + .getObject("customContent").getObject("listViewModel") |
| 1553 | + .getArray("listItems"); |
| 1554 | + return Optional.ofNullable(listItems.getObject(0) |
| 1555 | + .getObject("listItemViewModel", null)); |
| 1556 | + } |
1544 | 1557 | } |
0 commit comments