mirror of
https://github.com/iv-org/invidious.git
synced 2026-07-09 07:06:47 -05:00
fix(playlists): fall back to playlistHeaderRenderer when sidebar is absent
YouTube stopped returning sidebar.playlistSidebarRenderer for many playlists; extract the same fields from header.playlistHeaderRenderer instead to prevent 500 errors on /playlist pages. Also fix the -1 video count on lockupViewModel playlist cards: make badge text matching case-insensitive and add a numeric-prefix fallback so counts are recovered even when YouTube changes the badge wording.
This commit is contained in:
parent
347f84c276
commit
b426f74ba5
@ -345,8 +345,35 @@ def fetch_playlist(plid : String)
|
|||||||
initial_data = YoutubeAPI.browse("VL" + plid, params: "")
|
initial_data = YoutubeAPI.browse("VL" + plid, params: "")
|
||||||
|
|
||||||
playlist_sidebar_renderer = initial_data.dig?("sidebar", "playlistSidebarRenderer", "items")
|
playlist_sidebar_renderer = initial_data.dig?("sidebar", "playlistSidebarRenderer", "items")
|
||||||
raise InfoException.new("Could not extract playlistSidebarRenderer.") if !playlist_sidebar_renderer
|
header_renderer = initial_data.dig?("header", "playlistHeaderRenderer")
|
||||||
|
|
||||||
|
raise InfoException.new("Could not extract playlist data.") if playlist_sidebar_renderer.nil? && header_renderer.nil?
|
||||||
|
|
||||||
|
views = 0_i64
|
||||||
|
updated = Time.utc
|
||||||
|
video_count = 0
|
||||||
|
subtitle = extract_text(header_renderer.try &.["subtitle"]?)
|
||||||
|
|
||||||
|
# stats parsing is the same format in both sidebar and header renderers
|
||||||
|
stats_source = playlist_sidebar_renderer.try(&.dig?(0, "playlistSidebarPrimaryInfoRenderer", "stats")) ||
|
||||||
|
header_renderer.try(&.["stats"]?)
|
||||||
|
|
||||||
|
stats_source.try &.as_a.each do |stat|
|
||||||
|
text = stat["runs"]?.try &.as_a.map(&.["text"].as_s).join("") || stat["simpleText"]?.try &.as_s
|
||||||
|
next if !text
|
||||||
|
|
||||||
|
if text.includes? "video"
|
||||||
|
video_count = text.gsub(/\D/, "").to_i? || 0
|
||||||
|
elsif text.includes? "episode"
|
||||||
|
video_count = text.gsub(/\D/, "").to_i? || 0
|
||||||
|
elsif text.includes? "view"
|
||||||
|
views = text.gsub(/\D/, "").to_i64? || 0_i64
|
||||||
|
elsif !text.includes? "Pay to watch"
|
||||||
|
updated = decode_date(text.lchop("Last updated on ").lchop("Updated "))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if playlist_sidebar_renderer
|
||||||
playlist_info = playlist_sidebar_renderer.dig?(0, "playlistSidebarPrimaryInfoRenderer")
|
playlist_info = playlist_sidebar_renderer.dig?(0, "playlistSidebarPrimaryInfoRenderer")
|
||||||
raise InfoException.new("Could not extract playlist info") if !playlist_info
|
raise InfoException.new("Could not extract playlist info") if !playlist_info
|
||||||
|
|
||||||
@ -368,27 +395,6 @@ def fetch_playlist(plid : String)
|
|||||||
"thumbnail", "thumbnails", 0, "url"
|
"thumbnail", "thumbnails", 0, "url"
|
||||||
).try &.as_s
|
).try &.as_s
|
||||||
|
|
||||||
views = 0_i64
|
|
||||||
updated = Time.utc
|
|
||||||
video_count = 0
|
|
||||||
|
|
||||||
subtitle = extract_text(initial_data.dig?("header", "playlistHeaderRenderer", "subtitle"))
|
|
||||||
|
|
||||||
playlist_info["stats"]?.try &.as_a.each do |stat|
|
|
||||||
text = stat["runs"]?.try &.as_a.map(&.["text"].as_s).join("") || stat["simpleText"]?.try &.as_s
|
|
||||||
next if !text
|
|
||||||
|
|
||||||
if text.includes? "video"
|
|
||||||
video_count = text.gsub(/\D/, "").to_i? || 0
|
|
||||||
elsif text.includes? "episode"
|
|
||||||
video_count = text.gsub(/\D/, "").to_i? || 0
|
|
||||||
elsif text.includes? "view"
|
|
||||||
views = text.gsub(/\D/, "").to_i64? || 0_i64
|
|
||||||
elsif !text.includes? "Pay to watch"
|
|
||||||
updated = decode_date(text.lchop("Last updated on ").lchop("Updated "))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if playlist_sidebar_renderer.size < 2
|
if playlist_sidebar_renderer.size < 2
|
||||||
author = ""
|
author = ""
|
||||||
author_thumbnail = ""
|
author_thumbnail = ""
|
||||||
@ -404,6 +410,27 @@ def fetch_playlist(plid : String)
|
|||||||
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url").try &.as_s || ""
|
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url").try &.as_s || ""
|
||||||
ucid = author_info.dig?("title", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
|
ucid = author_info.dig?("title", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
# Sidebar gone — fall back to playlistHeaderRenderer (YouTube changed response format)
|
||||||
|
hr = header_renderer.not_nil!
|
||||||
|
|
||||||
|
title = extract_text(hr["title"]?) || ""
|
||||||
|
|
||||||
|
desc_item = hr["descriptionText"]? || hr["description"]?
|
||||||
|
description_txt = desc_item.try &.["runs"]?.try &.as_a
|
||||||
|
.map(&.["text"].as_s).join("") || desc_item.try &.["simpleText"]?.try &.as_s || ""
|
||||||
|
description_html = desc_item.try &.["runs"]?.try &.as_a
|
||||||
|
.try { |run| content_to_comment_html(run).try &.to_s } || "<p></p>"
|
||||||
|
|
||||||
|
thumbnail = hr.dig?("thumbnail", "thumbnails", 0, "url").try &.as_s
|
||||||
|
|
||||||
|
owner_run = hr.dig?("ownerText", "runs", 0)
|
||||||
|
author = owner_run.try &.["text"]?.try &.as_s || ""
|
||||||
|
ucid = owner_run.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
|
||||||
|
author_thumbnail = hr.dig?("channelAvatar", "decoratedAvatarViewModel", "avatar",
|
||||||
|
"avatarViewModel", "image", "sources", 0, "url").try &.as_s ||
|
||||||
|
hr.dig?("avatar", "thumbnails", 0, "url").try &.as_s || ""
|
||||||
|
end
|
||||||
|
|
||||||
return Playlist.new({
|
return Playlist.new({
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@ -675,13 +675,20 @@ private module Parsers
|
|||||||
#
|
#
|
||||||
# NOTE: this simplistic `.to_i` conversion might not work on larger
|
# NOTE: this simplistic `.to_i` conversion might not work on larger
|
||||||
# playlists and hasn't been tested.
|
# playlists and hasn't been tested.
|
||||||
video_count = 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
|
||||||
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node|
|
.compact_map(&.dig?("thumbnailBadgeViewModel", "text").try &.as_s)
|
||||||
{"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) }
|
|
||||||
})
|
# Prefer badges explicitly labeled "videos" or "episodes" (case-insensitive)
|
||||||
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
|
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user