diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java index 6aa88c67d..2926d3150 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java @@ -299,6 +299,9 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor { } else if (richItem.has("reelItemRenderer")) { commitReel(collector, richItem.getObject("reelItemRenderer"), channelVerifiedStatus, channelName, channelUrl); + } else if (richItem.has("shortsLockupViewModel")) { + commitShortsLockup(collector, richItem.getObject("shortsLockupViewModel"), + channelVerifiedStatus, channelName, channelUrl); } else if (richItem.has("playlistRenderer")) { commitPlaylist(collector, richItem.getObject("playlistRenderer"), channelVerifiedStatus, channelName, channelUrl); @@ -356,6 +359,30 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor { }); } + private static void commitShortsLockup(@Nonnull final MultiInfoItemsCollector collector, + @Nonnull final JsonObject shortsLockupViewModel, + @Nonnull final VerifiedStatus channelVerifiedStatus, + @Nullable final String channelName, + @Nullable final String channelUrl) { + collector.commit( + new YoutubeShortsLockupInfoItemExtractor(shortsLockupViewModel) { + @Override + public String getUploaderName() throws ParsingException { + return isNullOrEmpty(channelName) ? super.getUploaderName() : channelName; + } + + @Override + public String getUploaderUrl() throws ParsingException { + return isNullOrEmpty(channelUrl) ? super.getUploaderName() : channelUrl; + } + + @Override + public boolean isUploaderVerified() { + return channelVerifiedStatus == VerifiedStatus.VERIFIED; + } + }); + } + private void commitVideo(@Nonnull final MultiInfoItemsCollector collector, @Nonnull final TimeAgoParser timeAgoParser, @Nonnull final JsonObject jsonObject, diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeReelInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeReelInfoItemExtractor.java index 3b9326247..71c7440b4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeReelInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeReelInfoItemExtractor.java @@ -20,13 +20,19 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; /** - * A {@link StreamInfoItemExtractor} for YouTube's {@code reelItemRenderers}. + * A {@link StreamInfoItemExtractor} for YouTube's {@code reelItemRenderer}s. * *

- * {@code reelItemRenderers} are returned on YouTube for their short-form contents on almost every + * {@code reelItemRenderer}s were returned on YouTube for their short-form contents on almost every * place and every major client. They provide a limited amount of information and do not provide * the exact view count, any uploader info (name, URL, avatar, verified status) and the upload date. *

+ * + *

+ * At the time this documentation has been updated, they are being replaced by + * {@code shortsLockupViewModel}s. See {@link YoutubeShortsLockupInfoItemExtractor} for an + * extractor for this new UI data type. + *

*/ public class YoutubeReelInfoItemExtractor implements StreamInfoItemExtractor { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeShortsLockupInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeShortsLockupInfoItemExtractor.java new file mode 100644 index 000000000..eeaa82a5a --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeShortsLockupInfoItemExtractor.java @@ -0,0 +1,150 @@ +package org.schabi.newpipe.extractor.services.youtube.extractors; + +import com.grack.nanojson.JsonObject; +import org.schabi.newpipe.extractor.Image; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory; +import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.utils.Utils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.util.List; + +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getThumbnailsFromInfoItem; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +/** + * A {@link StreamInfoItemExtractor} for YouTube's {@code shortsLockupViewModel}s. + * + *

+ * {@code shortsLockupViewModel}s are returned on YouTube for their short-form contents on almost + * every place and every major client. They provide a limited amount of information and do not + * provide the exact view count, any uploader info (name, URL, avatar, verified status) and the + * upload date. + *

+ * + *

+ * At the time this documentation has been written, this data UI type is not fully used (rolled + * out), so {@code reelItemRenderer}s are also returned. See {@link YoutubeReelInfoItemExtractor} + * for an extractor for this UI data type. + *

+ */ +public class YoutubeShortsLockupInfoItemExtractor implements StreamInfoItemExtractor { + + @Nonnull + private final JsonObject shortsLockupViewModel; + + public YoutubeShortsLockupInfoItemExtractor(@Nonnull final JsonObject shortsLockupViewModel) { + this.shortsLockupViewModel = shortsLockupViewModel; + } + + @Override + public String getName() throws ParsingException { + return shortsLockupViewModel.getObject("overlayMetadata") + .getObject("primaryText") + .getString("content"); + } + + @Override + public String getUrl() throws ParsingException { + String videoId = shortsLockupViewModel.getObject("onTap") + .getObject("innertubeCommand") + .getObject("reelWatchEndpoint") + .getString("videoId"); + + if (isNullOrEmpty(videoId)) { + videoId = shortsLockupViewModel.getObject("inlinePlayerData") + .getObject("onVisible") + .getObject("innertubeCommand") + .getObject("watchEndpoint") + .getString("videoId"); + } + + if (isNullOrEmpty(videoId)) { + throw new ParsingException("Could not get video ID"); + } + + try { + return YoutubeStreamLinkHandlerFactory.getInstance().getUrl(videoId); + } catch (final Exception e) { + throw new ParsingException("Could not get URL", e); + } + } + + @Nonnull + @Override + public List getThumbnails() throws ParsingException { + return getThumbnailsFromInfoItem(shortsLockupViewModel.getObject("thumbnail") + .getObject("sources")); + } + + @Override + public StreamType getStreamType() throws ParsingException { + return StreamType.VIDEO_STREAM; + } + + @Override + public long getViewCount() throws ParsingException { + final String viewCountText = shortsLockupViewModel.getObject("overlayMetadata") + .getObject("secondaryText") + .getString("content"); + if (!isNullOrEmpty(viewCountText)) { + // This approach is language dependent + if (viewCountText.toLowerCase().contains("no views")) { + return 0; + } + + return Utils.mixedNumberWordToLong(viewCountText); + } + + throw new ParsingException("Could not get short view count"); + } + + @Override + public boolean isShortFormContent() { + return true; + } + + // All the following properties cannot be obtained from shortsLockupViewModels + + @Override + public boolean isAd() throws ParsingException { + return false; + } + + @Override + public long getDuration() throws ParsingException { + return -1; + } + + @Override + public String getUploaderName() throws ParsingException { + return null; + } + + @Override + public String getUploaderUrl() throws ParsingException { + return null; + } + + @Override + public boolean isUploaderVerified() throws ParsingException { + return false; + } + + @Nullable + @Override + public String getTextualUploadDate() throws ParsingException { + return null; + } + + @Nullable + @Override + public DateWrapper getUploadDate() throws ParsingException { + return null; + } +}