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
|
||||
|
||||
# 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.
|
||||
#
|
||||
# This structure is present since November 2024 on the "podcasts" and
|
||||
# "playlists" tabs of the channel page. It is usually encapsulated in either
|
||||
# This structure is present since November 2024 on the "podcasts", "playlists",
|
||||
# and "videos" tabs of the channel page. It is usually encapsulated in either
|
||||
# a richItemRenderer or a richGridRenderer.
|
||||
#
|
||||
module LockupViewModelParser
|
||||
@ -648,7 +648,16 @@ private module Parsers
|
||||
end
|
||||
|
||||
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?(
|
||||
"contentImage", "collectionThumbnailViewModel",
|
||||
@ -672,14 +681,23 @@ 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
|
||||
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
||||
.flatten
|
||||
.compact_map(&.dig?("thumbnailBadgeViewModel", "text").try &.as_s)
|
||||
|
||||
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
||||
title = metadata.dig("title", "content").as_s
|
||||
|
||||
# TODO: Retrieve "updated" info from metadata parts
|
||||
# rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").as_a
|
||||
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
|
||||
# One of these parts should contain a string like: "Updated 2 days ago"
|
||||
|
||||
if is_playlist
|
||||
# NOTE: this simplistic `.to_i` conversion might not work on larger
|
||||
# playlists and hasn't been tested.
|
||||
|
||||
# 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) } }
|
||||
@ -690,21 +708,9 @@ private module Parsers
|
||||
.find { |t| t[0]?.try &.ascii_number? }
|
||||
.try &.to_i(strict: false)
|
||||
|
||||
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
||||
title = metadata.dig("title", "content").as_s
|
||||
|
||||
# TODO: Retrieve "updated" info from metadata parts
|
||||
# rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").as_a
|
||||
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
|
||||
# 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?
|
||||
# item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint")
|
||||
# Available fields: "videoId", "playlistId", "params"
|
||||
|
||||
return SearchPlaylist.new({
|
||||
title: title,
|
||||
id: playlist_id,
|
||||
id: content_id,
|
||||
author: author_fallback.name,
|
||||
ucid: author_fallback.id,
|
||||
video_count: video_count || -1,
|
||||
@ -712,6 +718,27 @@ private module Parsers
|
||||
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
|
||||
|
||||
def self.parser_name
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user