diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemLockupExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemLockupExtractor.java index a05f7b96b..f685f33de 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemLockupExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemLockupExtractor.java @@ -17,6 +17,11 @@ import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Utils; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -30,20 +35,24 @@ import javax.annotation.Nullable; * The following features are currently not implemented because they have never been observed: * */ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtractor { private static final String NO_VIEWS_LOWERCASE = "no views"; + // This approach is language dependant (en-GB) + // Leading end space is voluntary included + private static final String PREMIERES_TEXT = "Premieres "; + private static final DateTimeFormatter PREMIERES_DATE_FORMATTER = + DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm"); private final JsonObject lockupViewModel; private final TimeAgoParser timeAgoParser; private StreamType cachedStreamType; private String cachedName; - private Optional cachedTextualUploadDate; + private Optional cachedDateText; private ChannelImageViewModel cachedChannelImageViewModel; private JsonArray cachedMetadataRows; @@ -137,7 +146,9 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra @Override public long getDuration() throws ParsingException { // Duration cannot be extracted for live streams, but only for normal videos - if (isLive()) { + // Exact duration cannot be extracted for premieres, an approximation is only available in + // accessibility context label + if (isLive() || isPremiere()) { return -1; } @@ -237,20 +248,27 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra @Nullable @Override public String getTextualUploadDate() throws ParsingException { - if (cachedTextualUploadDate != null) { - return cachedTextualUploadDate.orElse(null); - } - // Live streams have no upload date if (isLive()) { - cachedTextualUploadDate = Optional.empty(); return null; } - // This might be null e.g. for live streams - this.cachedTextualUploadDate = metadataPart(1, 1) - .map(this::getTextContentFromMetadataPart); - return cachedTextualUploadDate.orElse(null); + // Date string might be null e.g. for live streams + final Optional dateText = getDateText(); + + if (isPremiere()) { + return getDateFromPremiere(dateText); + } + + return dateText.orElse(null); + } + + @Nullable + private String getDateFromPremiere(final Optional dateText) { + // This approach is language dependent + // Remove the premieres text from the upload date metadata part + return dateText.map(str -> str.replace(PREMIERES_TEXT, "")) + .orElse(null); } @Nullable @@ -265,11 +283,32 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra if (textualUploadDate == null) { return null; } + + if (isPremiere()) { + final String premiereDate = getDateFromPremiere(getDateText()); + if (premiereDate == null) { + throw new ParsingException("Could not get upload date from premiere"); + } + + try { + // As we request a UTC offset of 0 minutes, we get the UTC date + return new DateWrapper(OffsetDateTime.of(LocalDateTime.parse( + premiereDate, PREMIERES_DATE_FORMATTER), ZoneOffset.UTC)); + } catch (final DateTimeParseException e) { + throw new ParsingException("Could not parse premiere upload date", e); + } + } + return timeAgoParser.parse(textualUploadDate); } @Override public long getViewCount() throws ParsingException { + if (isPremiere()) { + // The number of people returned for premieres is the one currently waiting + return -1; + } + final Optional optTextContent = metadataPart(1, 0) .map(this::getTextContentFromMetadataPart); // We could do this inline if the ParsingException would be a RuntimeException -.- @@ -357,6 +396,20 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra return getStreamType() != StreamType.VIDEO_STREAM; } + private Optional getDateText() throws ParsingException { + if (cachedDateText == null) { + cachedDateText = metadataPart(1, 1) + .map(this::getTextContentFromMetadataPart); + } + return cachedDateText; + } + + private boolean isPremiere() throws ParsingException { + return getDateText().map(dateText -> dateText.contains(PREMIERES_TEXT)) + // If we can't get date text, assume it is not a premiere, it should be a livestream + .orElse(false); + } + abstract static class ChannelImageViewModel { protected JsonObject viewModel; diff --git a/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderFactory.java b/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderFactory.java index eb9ee7cb4..07545b52b 100644 --- a/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderFactory.java +++ b/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderFactory.java @@ -4,6 +4,9 @@ import org.schabi.newpipe.extractor.downloader.Downloader; import java.util.Locale; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public class DownloaderFactory { private static final DownloaderType DEFAULT_DOWNLOADER = DownloaderType.REAL; @@ -36,20 +39,31 @@ public class DownloaderFactory { } public static Downloader getDownloader(final Class clazz) { - return getDownloader(clazz, null); + return getDownloader(getMockPath(clazz, null)); } - public static Downloader getDownloader(final Class clazz, final String specificUseCase) { + public static Downloader getDownloader(final Class clazz, + @Nullable final String specificUseCase) { + return getDownloader(getMockPath(clazz, specificUseCase)); + } + + /** + * Always returns a path without a trailing '/', so that it can be used both as a folder name + * and as a filename. The {@link MockDownloader} will use it as a folder name, but other tests + * can use it as a filename, if only one custom mock file is needed for that test. + */ + public static String getMockPath(final Class clazz, + @Nullable final String specificUseCase) { String baseName = clazz.getName(); if (specificUseCase != null) { baseName += "." + specificUseCase; } - return getDownloader("src/test/resources/mocks/v1/" - + baseName - .toLowerCase(Locale.ENGLISH) - .replace('$', '.') - .replace("test", "") - .replace('.', '/')); + return "src/test/resources/mocks/v1/" + + baseName + .toLowerCase(Locale.ENGLISH) + .replace('$', '.') + .replace("test", "") + .replace('.', '/'); } /** diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamInfoItemTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamInfoItemTest.java new file mode 100644 index 000000000..0782bbd56 --- /dev/null +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamInfoItemTest.java @@ -0,0 +1,83 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.schabi.newpipe.downloader.DownloaderFactory.getMockPath; + +import com.grack.nanojson.JsonParser; +import com.grack.nanojson.JsonParserException; + +import org.junit.jupiter.api.Test; +import org.schabi.newpipe.extractor.localization.Localization; +import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemExtractor; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemLockupExtractor; +import org.schabi.newpipe.extractor.stream.StreamType; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +public class YoutubeStreamInfoItemTest { + @Test + void videoRendererPremiere() throws FileNotFoundException, JsonParserException { + final var json = JsonParser.object().from(new FileInputStream(getMockPath( + YoutubeStreamInfoItemTest.class, "videoRendererPremiere") + ".json")); + final var timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.DEFAULT); + final var extractor = new YoutubeStreamInfoItemExtractor(json, timeAgoParser); + assertAll( + () -> assertEquals(StreamType.VIDEO_STREAM, extractor.getStreamType()), + () -> assertFalse(extractor.isAd()), + () -> assertEquals("https://www.youtube.com/watch?v=M_8QNw_JM4I", extractor.getUrl()), + () -> assertEquals("This video will premiere in 6 months.", extractor.getName()), + () -> assertEquals(33, extractor.getDuration()), + () -> assertEquals("Blunt Brothers Productions", extractor.getUploaderName()), + () -> assertEquals("https://www.youtube.com/channel/UCUPrbbdnot-aPgNM65svgOg", extractor.getUploaderUrl()), + () -> assertFalse(extractor.getUploaderAvatars().isEmpty()), + () -> assertTrue(extractor.isUploaderVerified()), + () -> assertEquals("2026-03-15 13:12", extractor.getTextualUploadDate()), + () -> { + assertNotNull(extractor.getUploadDate()); + assertEquals(OffsetDateTime.of(2026, 3, 15, 13, 12, 0, 0, ZoneOffset.UTC), extractor.getUploadDate().offsetDateTime()); + }, + () -> assertEquals(-1, extractor.getViewCount()), + () -> assertFalse(extractor.getThumbnails().isEmpty()), + () -> assertEquals("Patience is key… MERCH SHOP : https://www.bluntbrosproductions.com Follow us on Instagram for early updates: ...", extractor.getShortDescription()), + () -> assertFalse(extractor.isShortFormContent()) + ); + } + + @Test + void lockupViewModelPremiere() + throws FileNotFoundException, JsonParserException { + final var json = JsonParser.object().from(new FileInputStream(getMockPath( + YoutubeStreamInfoItemTest.class, "lockupViewModelPremiere") + ".json")); + final var timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.DEFAULT); + final var extractor = new YoutubeStreamInfoItemLockupExtractor(json, timeAgoParser); + assertAll( + () -> assertEquals(StreamType.VIDEO_STREAM, extractor.getStreamType()), + () -> assertFalse(extractor.isAd()), + () -> assertEquals("https://www.youtube.com/watch?v=VIDEO_ID", extractor.getUrl()), + () -> assertEquals("VIDEO_TITLE", extractor.getName()), + () -> assertEquals(-1, extractor.getDuration()), + () -> assertEquals("VIDEO_CHANNEL_NAME", extractor.getUploaderName()), + () -> assertEquals("https://www.youtube.com/channel/UCD_on7-zu7Zuc3zissQvrgw", extractor.getUploaderUrl()), + () -> assertFalse(extractor.getUploaderAvatars().isEmpty()), + () -> assertFalse(extractor.isUploaderVerified()), + () -> assertEquals("14/08/2025, 13:00", extractor.getTextualUploadDate()), + () -> { + assertNotNull(extractor.getUploadDate()); + assertEquals(OffsetDateTime.of(2025, 8, 14, 13, 0, 0, 0, ZoneOffset.UTC), extractor.getUploadDate().offsetDateTime()); + }, + () -> assertEquals(-1, extractor.getViewCount()), + () -> assertFalse(extractor.getThumbnails().isEmpty()), + () -> assertNull(extractor.getShortDescription()), + () -> assertFalse(extractor.isShortFormContent()) + ); + } +} diff --git a/extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/lockupviewmodelpremiere.json b/extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/lockupviewmodelpremiere.json new file mode 100644 index 000000000..836f704ff --- /dev/null +++ b/extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/lockupviewmodelpremiere.json @@ -0,0 +1,133 @@ +{ + "contentImage": { + "thumbnailViewModel": { + "image": { + "sources": [ + { + "url": "https://i.ytimg.com/vi/0_-Nh-nOhLQ/hqdefault.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "width": 168, + "height": 94 + }, + { + "url": "https://i.ytimg.com/vi/0_-Nh-nOhLQ/hqdefault.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "width": 336, + "height": 188 + } + ] + }, + "overlays": [ + { + "thumbnailOverlayBadgeViewModel": { + "thumbnailBadges": [ + { + "thumbnailBadgeViewModel": { + "text": "Upcoming", + "badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT", + "animationActivationTargetId": "VIDEO_ID", + "animationActivationEntityKey": "REDACTED", + "lottieData": { + "url": "https://www.gstatic.com/youtube/img/lottie/audio_indicator/audio_indicator_v2.json", + "settings": { + "loop": true, + "autoplay": true + } + }, + "animatedText": "Now playing", + "animationActivationEntitySelectorType": "THUMBNAIL_BADGE_ANIMATION_ENTITY_SELECTOR_TYPE_PLAYER_STATE" + } + } + ], + "position": "THUMBNAIL_OVERLAY_BADGE_POSITION_BOTTOM_END" + } + }, + { + "thumbnailHoverOverlayToggleActionsViewModel": { + "buttons": [] + } + } + ] + } + }, + "metadata": { + "lockupMetadataViewModel": { + "title": { + "content": "VIDEO_TITLE" + }, + "image": { + "decoratedAvatarViewModel": { + "avatar": { + "avatarViewModel": { + "image": { + "sources": [ + { + "url": "https://yt3.ggpht.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "width": 68, + "height": 68 + } + ] + }, + "avatarImageSize": "AVATAR_SIZE_M" + } + }, + "a11yLabel": "Go to channel", + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/@deep_flow_music_ua", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCD_on7-zu7Zuc3zissQvrgw", + "canonicalBaseUrl": "/@deep_flow_music_ua" + } + } + } + } + } + } + }, + "metadata": { + "contentMetadataViewModel": { + "metadataRows": [ + { + "metadataParts": [ + { + "text": { + "content": "VIDEO_CHANNEL_NAME", + "styleRuns": [], + "attachmentRuns": [] + } + } + ] + }, + { + "metadataParts": [ + { + "text": { + "content": "56 waiting" + } + }, + { + "text": { + "content": "Premieres 14/08/2025, 13:00" + } + } + ] + } + ], + "delimiter": " • " + } + }, + "menuButton": {} + } + }, + "contentId": "VIDEO_ID", + "contentType": "LOCKUP_CONTENT_TYPE_VIDEO" +} diff --git a/extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/videorendererpremiere.json b/extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/videorendererpremiere.json new file mode 100644 index 000000000..35e85055d --- /dev/null +++ b/extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/videorendererpremiere.json @@ -0,0 +1,586 @@ +{ + "videoId": "M_8QNw_JM4I", + "thumbnail": { + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/M_8QNw_JM4I/hq720.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "width": 360, + "height": 202 + }, + { + "url": "https://i.ytimg.com/vi/M_8QNw_JM4I/hq720.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "width": 720, + "height": 404 + } + ] + }, + "title": { + "runs": [ + { + "text": "This video will premiere in 6 months." + } + ], + "accessibility": { + "accessibilityData": { + "label": "This video will premiere in 6 months. 33 seconds" + } + } + }, + "descriptionSnippet": { + "runs": [ + { + "text": "Patience is key… MERCH SHOP : https://www.bluntbrosproductions.com Follow us on Instagram for early updates: ..." + } + ] + }, + "longBylineText": { + "runs": [ + { + "text": "Blunt Brothers Productions", + "navigationEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/@BluntBrothersProductions", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCUPrbbdnot-aPgNM65svgOg", + "canonicalBaseUrl": "/@BluntBrothersProductions" + } + } + } + ] + }, + "lengthText": { + "accessibility": { + "accessibilityData": { + "label": "33 seconds" + } + }, + "simpleText": "0:33" + }, + "navigationEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/watch?v=M_8QNw_JM4I&pp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "webPageType": "WEB_PAGE_TYPE_WATCH", + "rootVe": 3832 + } + }, + "watchEndpoint": { + "videoId": "M_8QNw_JM4I", + "params": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "playerParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "watchEndpointSupportedOnesieConfig": { + "html5PlaybackOnesieConfig": { + "commonConfig": { + "url": "https://rr4---sn-n0ogpnx-1gie.googlevideo.com/initplayback?source=youtube&oeis=1&c=WEB&oad=3200&ovd=3200&oaad=11000&oavd=11000&ocs=700&oewis=1&oputc=1&ofpcc=1&msp=1&odepv=1&oreouc=1&id=33ff10370fc93382&ip=1.1.1.1&initcwndbps=3320000&mt=1759654189&oweuc=" + } + } + } + } + }, + "ownerBadges": [ + { + "metadataBadgeRenderer": { + "icon": { + "iconType": "CHECK_CIRCLE_THICK" + }, + "style": "BADGE_STYLE_TYPE_VERIFIED", + "tooltip": "Verified", + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "accessibilityData": { + "label": "Verified" + } + } + } + ], + "ownerText": { + "runs": [ + { + "text": "Blunt Brothers Productions", + "navigationEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/@BluntBrothersProductions", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCUPrbbdnot-aPgNM65svgOg", + "canonicalBaseUrl": "/@BluntBrothersProductions" + } + } + } + ] + }, + "upcomingEventData": { + "startTime": "1773580320", + "isReminderSet": false, + "upcomingEventText": { + "runs": [ + { + "text": "Premieres " + }, + { + "text": "DATE_PLACEHOLDER" + } + ] + } + }, + "shortBylineText": { + "runs": [ + { + "text": "Blunt Brothers Productions", + "navigationEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/@BluntBrothersProductions", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCUPrbbdnot-aPgNM65svgOg", + "canonicalBaseUrl": "/@BluntBrothersProductions" + } + } + } + ] + }, + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "showActionMenu": false, + "shortViewCountText": { + "runs": [ + { + "text": "1" + }, + { + "text": " waiting" + } + ], + "accessibility": { + "accessibilityData": { + "label": "1 waiting" + } + } + }, + "menu": { + "menuRenderer": { + "items": [ + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_QUEUE_TAIL" + }, + "serviceEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true + } + }, + "signalServiceEndpoint": { + "signal": "CLIENT_SIGNAL", + "actions": [ + { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "addToPlaylistCommand": { + "openMiniplayer": true, + "videoId": "M_8QNw_JM4I", + "listType": "PLAYLIST_EDIT_LIST_TYPE_QUEUE", + "onCreateListCommand": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true, + "apiUrl": "/youtubei/v1/playlist/create" + } + }, + "createPlaylistServiceEndpoint": { + "videoIds": [ + "M_8QNw_JM4I" + ], + "params": "CAQ%3D" + } + }, + "videoIds": [ + "M_8QNw_JM4I" + ], + "videoCommand": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/watch?v=M_8QNw_JM4I", + "webPageType": "WEB_PAGE_TYPE_WATCH", + "rootVe": 3832 + } + }, + "watchEndpoint": { + "videoId": "M_8QNw_JM4I", + "watchEndpointSupportedOnesieConfig": { + "html5PlaybackOnesieConfig": { + "commonConfig": { + "url": "https://rr4---sn-n0ogpnx-1gie.googlevideo.com/initplayback?source=youtube&oeis=1&c=WEB&oad=3200&ovd=3200&oaad=11000&oavd=11000&ocs=700&oewis=1&oputc=1&ofpcc=1&msp=1&odepv=1&oreouc=1&id=33ff10370fc93382&ip=1.1.1.1&initcwndbps=3320000&mt=1759654189&oweuc=" + } + } + } + } + } + } + } + ] + } + }, + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + }, + { + "menuServiceItemDownloadRenderer": { + "serviceEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "offlineVideoEndpoint": { + "videoId": "M_8QNw_JM4I", + "onAddCommand": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "getDownloadActionCommand": { + "videoId": "M_8QNw_JM4I", + "params": "CAIQAA%3D%3D" + } + } + } + }, + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "serviceEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true, + "apiUrl": "/youtubei/v1/share/get_share_panel" + } + }, + "shareEntityServiceEndpoint": { + "serializedShareEntity": "CgtNXzhRTndfSk00SQ%3D%3D", + "commands": [ + { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "openPopupAction": { + "popup": { + "unifiedSharePanelRenderer": { + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "showLoadingSpinner": true + } + }, + "popupType": "DIALOG", + "beReused": true + } + } + ] + } + }, + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "hasSeparator": true + } + } + ], + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "channelThumbnailSupportedRenderers": { + "channelThumbnailWithLinkRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://yt3.ggpht.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "width": 68, + "height": 68 + } + ] + }, + "navigationEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/@BluntBrothersProductions", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCUPrbbdnot-aPgNM65svgOg", + "canonicalBaseUrl": "/@BluntBrothersProductions" + } + }, + "accessibility": { + "accessibilityData": { + "label": "Go to channel" + } + } + } + }, + "thumbnailOverlays": [ + { + "thumbnailOverlayTimeStatusRenderer": { + "text": { + "accessibility": { + "accessibilityData": { + "label": "Upcoming" + } + }, + "simpleText": "UPCOMING" + }, + "style": "UPCOMING" + } + }, + { + "thumbnailOverlayToggleButtonRenderer": { + "isToggled": false, + "untoggledIcon": { + "iconType": "WATCH_LATER" + }, + "toggledIcon": { + "iconType": "CHECK" + }, + "untoggledTooltip": "Watch later", + "toggledTooltip": "Added", + "untoggledServiceEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true, + "apiUrl": "/youtubei/v1/browse/edit_playlist" + } + }, + "playlistEditEndpoint": { + "playlistId": "WL", + "actions": [ + { + "addedVideoId": "M_8QNw_JM4I", + "action": "ACTION_ADD_VIDEO" + } + ] + } + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true, + "apiUrl": "/youtubei/v1/browse/edit_playlist" + } + }, + "playlistEditEndpoint": { + "playlistId": "WL", + "actions": [ + { + "action": "ACTION_REMOVE_VIDEO_BY_VIDEO_ID", + "removedVideoId": "M_8QNw_JM4I" + } + ] + } + }, + "untoggledAccessibility": { + "accessibilityData": { + "label": "Watch later" + } + }, + "toggledAccessibility": { + "accessibilityData": { + "label": "Added" + } + }, + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + }, + { + "thumbnailOverlayToggleButtonRenderer": { + "untoggledIcon": { + "iconType": "ADD_TO_QUEUE_TAIL" + }, + "toggledIcon": { + "iconType": "PLAYLIST_ADD_CHECK" + }, + "untoggledTooltip": "Add to queue", + "toggledTooltip": "Added", + "untoggledServiceEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true + } + }, + "signalServiceEndpoint": { + "signal": "CLIENT_SIGNAL", + "actions": [ + { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "addToPlaylistCommand": { + "openMiniplayer": true, + "videoId": "M_8QNw_JM4I", + "listType": "PLAYLIST_EDIT_LIST_TYPE_QUEUE", + "onCreateListCommand": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "sendPost": true, + "apiUrl": "/youtubei/v1/playlist/create" + } + }, + "createPlaylistServiceEndpoint": { + "videoIds": [ + "M_8QNw_JM4I" + ], + "params": "CAQ%3D" + } + }, + "videoIds": [ + "M_8QNw_JM4I" + ] + } + } + ] + } + }, + "untoggledAccessibility": { + "accessibilityData": { + "label": "Add to queue" + } + }, + "toggledAccessibility": { + "accessibilityData": { + "label": "Added" + } + }, + "trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + }, + { + "thumbnailOverlayNowPlayingRenderer": { + "text": { + "runs": [ + { + "text": "Now playing" + } + ] + } + } + }, + { + "thumbnailOverlayLoadingPreviewRenderer": { + "text": { + "runs": [ + { + "text": "Keep hovering to play" + } + ] + } + } + } + ], + "inlinePlaybackEndpoint": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/watch?v=M_8QNw_JM4I&pp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "webPageType": "WEB_PAGE_TYPE_WATCH", + "rootVe": 3832 + } + }, + "watchEndpoint": { + "videoId": "M_8QNw_JM4I", + "params": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "playerParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "playerExtraUrlParams": [ + { + "key": "inline", + "value": "1" + } + ], + "watchEndpointSupportedOnesieConfig": { + "html5PlaybackOnesieConfig": { + "commonConfig": { + "url": "https://rr4---sn-n0ogpnx-1gie.googlevideo.com/initplayback?source=youtube&oeis=1&c=WEB&oad=3200&ovd=3200&oaad=11000&oavd=11000&ocs=700&oewis=1&oputc=1&ofpcc=1&msp=1&odepv=1&oreouc=1&id=33ff10370fc93382&ip=1.1.1.1&initcwndbps=3320000&mt=1759654189&oweuc=" + } + } + } + } + }, + "searchVideoResultEntityKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "avatar": { + "decoratedAvatarViewModel": { + "avatar": { + "avatarViewModel": { + "image": { + "sources": [ + { + "url": "https://yt3.ggpht.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "width": 68, + "height": 68 + } + ] + }, + "avatarImageSize": "AVATAR_SIZE_M" + } + }, + "a11yLabel": "Go to channel", + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "commandMetadata": { + "webCommandMetadata": { + "url": "/@BluntBrothersProductions", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCUPrbbdnot-aPgNM65svgOg", + "canonicalBaseUrl": "/@BluntBrothersProductions" + } + } + } + } + } + } + } +}