summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamantaz Fox <coding@samantaz.fr>2024-11-08 13:32:44 +0100
committerSamantaz Fox <coding@samantaz.fr>2024-11-08 14:00:30 +0100
commitafc5b27d83d8b2b287842ed1ec43185135441d37 (patch)
tree4a5e1ae5475c1238f792acedc2ce71b60dd0f079
parent1a5047aad94454fd8a8d9623e17ee3782c68c3d0 (diff)
downloadinvidious-afc5b27d83d8b2b287842ed1ec43185135441d37.tar.gz
invidious-afc5b27d83d8b2b287842ed1ec43185135441d37.tar.bz2
invidious-afc5b27d83d8b2b287842ed1ec43185135441d37.zip
Extractors: Add support for shortsLockupViewModel
The 'shortsLockupViewModel' structure is used in the channel "shorts" tab
-rw-r--r--src/invidious/yt_backend/extractors.cr58
1 files changed, 58 insertions, 0 deletions
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index cb8331a5..4416ef30 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -483,6 +483,7 @@ private module Parsers
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
child ||= LockupViewModelParser.process(item_contents, author_fallback)
+ child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback)
return child
end
@@ -497,6 +498,9 @@ private module Parsers
# reelItemRenderer items are used in the new (2022) channel layout,
# in the "shorts" tab.
#
+ # NOTE: As of 10/2024, it might have been fully replaced by shortsLockupViewModel
+ # TODO: Confirm that hypothesis
+ #
module ReelItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["reelItemRenderer"]?
@@ -652,6 +656,60 @@ private module Parsers
end
end
+ # Parses an InnerTube shortsLockupViewModel into a SearchVideo.
+ # Returns nil when the given object is not a shortsLockupViewModel.
+ #
+ # This structure is present since around October 2024 on the "shorts" tab of
+ # the channel page and likely replaces the reelItemRenderer structure. It is
+ # usually (always?) encapsulated in a richItemRenderer.
+ #
+ module ShortsLockupViewModelParser
+ def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ if item_contents = item["shortsLockupViewModel"]?
+ return self.parse(item_contents, author_fallback)
+ end
+ end
+
+ private def self.parse(item_contents, author_fallback)
+ # TODO: Maybe add support for "oardefault.jpg" thumbnails?
+ # thumbnail = item_contents.dig("thumbnail", "sources", 0, "url").as_s
+ # Gives: https://i.ytimg.com/vi/{video_id}/oardefault.jpg?...
+
+ video_id = item_contents.dig(
+ "onTap", "innertubeCommand", "reelWatchEndpoint", "videoId"
+ ).as_s
+
+ title = item_contents.dig("overlayMetadata", "primaryText", "content").as_s
+
+ view_count = short_text_to_number(
+ item_contents.dig("overlayMetadata", "secondaryText", "content").as_s
+ )
+
+ # Approximate to one minute, as "shorts" generally don't exceed that.
+ # NOTE: The actual duration is not provided by Youtube anymore.
+ # TODO: Maybe use -1 as an error value and handle that on the frontend?
+ duration = 60_i32
+
+ SearchVideo.new({
+ title: title,
+ id: video_id,
+ author: author_fallback.name,
+ ucid: author_fallback.id,
+ published: Time.unix(0),
+ views: view_count,
+ description_html: "",
+ length_seconds: duration,
+ premiere_timestamp: Time.unix(0),
+ author_verified: false,
+ badges: VideoBadges::None,
+ })
+ end
+
+ def self.parser_name
+ return {{@type.name}}
+ end
+ end
+
# Parses an InnerTube continuationItemRenderer into a Continuation.
# Returns nil when the given object isn't a continuationItemRenderer.
#