From 7673ea07b60fcc9bfa811da96ae6387682b608ca Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:49:20 +0200 Subject: [PATCH 1/5] [YouTube] Add support for premieres in lockupViewModels --- .../YoutubeStreamInfoItemLockupExtractor.java | 80 ++++++++++++++++--- 1 file changed, 68 insertions(+), 12 deletions(-) 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..5866c5c51 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,10 @@ 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.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -30,20 +34,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 +145,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 +247,37 @@ 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()) { + final LocalDateTime premiereDate = getDateFromPremiere(dateText); + if (premiereDate == null) { + return null; + } + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(premiereDate); + } + + return dateText.orElse(null); + } + + private LocalDateTime getDateFromPremiere(final Optional dateText) { + // This approach is language dependent + // Remove the premieres text from the upload date metadata part + final String trimmedTextUploadDate = + dateText.map(str -> str.replace(PREMIERES_TEXT, "")) + .orElse(null); + if (trimmedTextUploadDate == null) { + return null; + } + + // As we request a UTC offset of 0 minutes, we get the UTC date + return LocalDateTime.parse(trimmedTextUploadDate, PREMIERES_DATE_FORMATTER); } @Nullable @@ -265,11 +292,26 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra if (textualUploadDate == null) { return null; } + + if (isPremiere()) { + final LocalDateTime premiereDate = getDateFromPremiere(getDateText()); + if (premiereDate == null) { + throw new ParsingException("Could not get upload date from premiere"); + } + + return new DateWrapper(OffsetDateTime.of(premiereDate, ZoneOffset.UTC)); + } + 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 +399,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; From e50f4997979b6a8daf8a0fffc7042b1de4a24aa9 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 5 Oct 2025 13:44:23 +0200 Subject: [PATCH 2/5] [YouTube] Return orignial text date in lockupViewModels premieres --- .../YoutubeStreamInfoItemLockupExtractor.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) 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 5866c5c51..88a027d4b 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 @@ -256,28 +256,18 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra final Optional dateText = getDateText(); if (isPremiere()) { - final LocalDateTime premiereDate = getDateFromPremiere(dateText); - if (premiereDate == null) { - return null; - } - return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(premiereDate); + return getDateFromPremiere(dateText); } return dateText.orElse(null); } - private LocalDateTime getDateFromPremiere(final Optional dateText) { + @Nullable + private String getDateFromPremiere(final Optional dateText) { // This approach is language dependent // Remove the premieres text from the upload date metadata part - final String trimmedTextUploadDate = - dateText.map(str -> str.replace(PREMIERES_TEXT, "")) + return dateText.map(str -> str.replace(PREMIERES_TEXT, "")) .orElse(null); - if (trimmedTextUploadDate == null) { - return null; - } - - // As we request a UTC offset of 0 minutes, we get the UTC date - return LocalDateTime.parse(trimmedTextUploadDate, PREMIERES_DATE_FORMATTER); } @Nullable @@ -294,12 +284,14 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra } if (isPremiere()) { - final LocalDateTime premiereDate = getDateFromPremiere(getDateText()); + final String premiereDate = getDateFromPremiere(getDateText()); if (premiereDate == null) { throw new ParsingException("Could not get upload date from premiere"); } - return new DateWrapper(OffsetDateTime.of(premiereDate, ZoneOffset.UTC)); + // As we request a UTC offset of 0 minutes, we get the UTC data + return new DateWrapper(OffsetDateTime.of( + LocalDateTime.parse(premiereDate, PREMIERES_DATE_FORMATTER), ZoneOffset.UTC)); } return timeAgoParser.parse(textualUploadDate); From 3c404e575af66de3b3a95248994ba44ef833ac7a Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 5 Oct 2025 13:45:38 +0200 Subject: [PATCH 3/5] [YouTube] Add custom mock tests for Premiere StreamInfoItems The JSON data for these tests was obtained manually. --- .../newpipe/downloader/DownloaderFactory.java | 30 +- .../youtube/YoutubeStreamInfoItemTest.java | 83 +++ .../lockupviewmodelpremiere.json | 133 ++++ .../videorendererpremiere.json | 586 ++++++++++++++++++ 4 files changed, 824 insertions(+), 8 deletions(-) create mode 100644 extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamInfoItemTest.java create mode 100644 extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/lockupviewmodelpremiere.json create mode 100644 extractor/src/test/resources/mocks/v1/org/schabi/newpipe/extractor/services/youtube/youtubestreaminfoitem/videorendererpremiere.json 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..9a7d1b862 --- /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" + } + } + } + } + } + } + } +} \ No newline at end of file From 393e811470c29e1c76bc391a4457c73694559f6e Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 5 Oct 2025 13:53:10 +0200 Subject: [PATCH 4/5] Wrap LocalDateTime.parse in try-catch --- .../YoutubeStreamInfoItemLockupExtractor.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 88a027d4b..4ee2ae08e 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 @@ -21,6 +21,7 @@ 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; @@ -289,9 +290,13 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra throw new ParsingException("Could not get upload date from premiere"); } - // As we request a UTC offset of 0 minutes, we get the UTC data - return new DateWrapper(OffsetDateTime.of( - LocalDateTime.parse(premiereDate, PREMIERES_DATE_FORMATTER), ZoneOffset.UTC)); + try { + // As we request a UTC offset of 0 minutes, we get the UTC data + 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); From 97957250aa632a637b1a8c2ce023af6bc6b52a57 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 5 Oct 2025 14:13:46 +0200 Subject: [PATCH 5/5] Address review comments --- .../extractors/YoutubeStreamInfoItemLockupExtractor.java | 2 +- .../youtube/youtubestreaminfoitem/videorendererpremiere.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 4ee2ae08e..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 @@ -291,7 +291,7 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra } try { - // As we request a UTC offset of 0 minutes, we get the UTC data + // 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) { 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 index 9a7d1b862..35e85055d 100644 --- 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 @@ -583,4 +583,4 @@ } } } -} \ No newline at end of file +}