mirror of
https://github.com/iv-org/invidious.git
synced 2026-07-07 14:16:45 -05:00
fix(extractors): distinguish video vs playlist in LockupViewModelParser
Video IDs are always 11 characters; playlists have a playlistId in their watchEndpoint. Previously all lockupViewModel items were returned as SearchPlaylist, causing video links on channel pages to route to /playlist?list= instead of /watch?v=. Authored-by: NorkzYT <richard@pcscorp.dev>
This commit is contained in:
parent
b426f74ba5
commit
fc935d5175
@ -630,11 +630,11 @@ private module Parsers
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Parses an InnerTube lockupViewModel into a SearchPlaylist.
|
# Parses an InnerTube lockupViewModel into a SearchPlaylist or SearchVideo.
|
||||||
# Returns nil when the given object is not a lockupViewModel.
|
# Returns nil when the given object is not a lockupViewModel.
|
||||||
#
|
#
|
||||||
# This structure is present since November 2024 on the "podcasts" and
|
# This structure is present since November 2024 on the "podcasts", "playlists",
|
||||||
# "playlists" tabs of the channel page. It is usually encapsulated in either
|
# and "videos" tabs of the channel page. It is usually encapsulated in either
|
||||||
# a richItemRenderer or a richGridRenderer.
|
# a richItemRenderer or a richGridRenderer.
|
||||||
#
|
#
|
||||||
module LockupViewModelParser
|
module LockupViewModelParser
|
||||||
@ -648,7 +648,16 @@ private module Parsers
|
|||||||
end
|
end
|
||||||
|
|
||||||
private def parse_internal(item_contents, author_fallback)
|
private def parse_internal(item_contents, author_fallback)
|
||||||
playlist_id = item_contents["contentId"].as_s
|
content_id = item_contents["contentId"].as_s
|
||||||
|
|
||||||
|
# A watchEndpoint with a playlistId means this item is a playlist.
|
||||||
|
# If it only has a videoId (no playlistId), it's a regular video.
|
||||||
|
# Fall back to content_id length: video IDs are always exactly 11 chars.
|
||||||
|
watch_endpoint = item_contents.dig?(
|
||||||
|
"rendererContext", "commandContext", "onTap",
|
||||||
|
"innertubeCommand", "watchEndpoint"
|
||||||
|
)
|
||||||
|
is_playlist = watch_endpoint.try(&.["playlistId"]?) || content_id.size != 11
|
||||||
|
|
||||||
thumbnail_view_model = item_contents.dig?(
|
thumbnail_view_model = item_contents.dig?(
|
||||||
"contentImage", "collectionThumbnailViewModel",
|
"contentImage", "collectionThumbnailViewModel",
|
||||||
@ -672,24 +681,11 @@ private module Parsers
|
|||||||
# }]
|
# }]
|
||||||
# }
|
# }
|
||||||
# }]
|
# }]
|
||||||
#
|
|
||||||
# NOTE: this simplistic `.to_i` conversion might not work on larger
|
|
||||||
# playlists and hasn't been tested.
|
|
||||||
all_badge_texts = thumbnail_view_model.dig("overlays").as_a
|
all_badge_texts = thumbnail_view_model.dig("overlays").as_a
|
||||||
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
||||||
.flatten
|
.flatten
|
||||||
.compact_map(&.dig?("thumbnailBadgeViewModel", "text").try &.as_s)
|
.compact_map(&.dig?("thumbnailBadgeViewModel", "text").try &.as_s)
|
||||||
|
|
||||||
# Prefer badges explicitly labeled "videos" or "episodes" (case-insensitive)
|
|
||||||
video_count = all_badge_texts
|
|
||||||
.find { |t| {"episodes", "videos"}.any? { |s| t.downcase.ends_with?(s) } }
|
|
||||||
.try &.to_i(strict: false)
|
|
||||||
|
|
||||||
# Fallback: any badge whose text starts with a digit
|
|
||||||
video_count ||= all_badge_texts
|
|
||||||
.find { |t| t[0]?.try &.ascii_number? }
|
|
||||||
.try &.to_i(strict: false)
|
|
||||||
|
|
||||||
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
||||||
title = metadata.dig("title", "content").as_s
|
title = metadata.dig("title", "content").as_s
|
||||||
|
|
||||||
@ -698,20 +694,51 @@ private module Parsers
|
|||||||
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
|
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
|
||||||
# One of these parts should contain a string like: "Updated 2 days ago"
|
# One of these parts should contain a string like: "Updated 2 days ago"
|
||||||
|
|
||||||
# TODO: Maybe add a button to access the first video of the playlist?
|
if is_playlist
|
||||||
# item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint")
|
# NOTE: this simplistic `.to_i` conversion might not work on larger
|
||||||
# Available fields: "videoId", "playlistId", "params"
|
# playlists and hasn't been tested.
|
||||||
|
|
||||||
return SearchPlaylist.new({
|
# Prefer badges explicitly labeled "videos" or "episodes" (case-insensitive)
|
||||||
title: title,
|
video_count = all_badge_texts
|
||||||
id: playlist_id,
|
.find { |t| {"episodes", "videos"}.any? { |s| t.downcase.ends_with?(s) } }
|
||||||
author: author_fallback.name,
|
.try &.to_i(strict: false)
|
||||||
ucid: author_fallback.id,
|
|
||||||
video_count: video_count || -1,
|
# Fallback: any badge whose text starts with a digit
|
||||||
videos: [] of SearchPlaylistVideo,
|
video_count ||= all_badge_texts
|
||||||
thumbnail: thumbnail,
|
.find { |t| t[0]?.try &.ascii_number? }
|
||||||
author_verified: false,
|
.try &.to_i(strict: false)
|
||||||
})
|
|
||||||
|
return SearchPlaylist.new({
|
||||||
|
title: title,
|
||||||
|
id: content_id,
|
||||||
|
author: author_fallback.name,
|
||||||
|
ucid: author_fallback.id,
|
||||||
|
video_count: video_count || -1,
|
||||||
|
videos: [] of SearchPlaylistVideo,
|
||||||
|
thumbnail: thumbnail,
|
||||||
|
author_verified: false,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
# Extract duration from a time-format badge (e.g. "5:30", "1:23:45")
|
||||||
|
length_seconds = all_badge_texts
|
||||||
|
.find { |t| /^\d+:\d+/.match(t) }
|
||||||
|
.try { |t| decode_length_seconds(t) } || 0
|
||||||
|
|
||||||
|
return SearchVideo.new({
|
||||||
|
title: title,
|
||||||
|
id: content_id,
|
||||||
|
author: author_fallback.name,
|
||||||
|
ucid: author_fallback.id,
|
||||||
|
published: Time.unix(0),
|
||||||
|
views: 0_i64,
|
||||||
|
description_html: "",
|
||||||
|
length_seconds: length_seconds,
|
||||||
|
premiere_timestamp: Time.unix(0),
|
||||||
|
author_verified: false,
|
||||||
|
author_thumbnail: nil,
|
||||||
|
badges: VideoBadges::None,
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parser_name
|
def self.parser_name
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user