From c81452f2a4ee165442b6785c9b3a18d35d4a7386 Mon Sep 17 00:00:00 2001 From: EazyHood <209367218+EazyHood@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:10:56 -0500 Subject: [PATCH] channels: handle incomplete auto-generated payloads --- spec/invidious/channels/about_spec.cr | 29 +++++++++++++++++--- spec/invidious/yt_backend/extractors_spec.cr | 22 +++++++++++++++ src/invidious/channels/about.cr | 10 +++++-- src/invidious/yt_backend/extractors.cr | 3 +- 4 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 spec/invidious/yt_backend/extractors_spec.cr diff --git a/spec/invidious/channels/about_spec.cr b/spec/invidious/channels/about_spec.cr index 66f8e029..abf21809 100644 --- a/spec/invidious/channels/about_spec.cr +++ b/spec/invidious/channels/about_spec.cr @@ -69,7 +69,7 @@ Spectator.describe "extract_auto_generated_channel_header" do expect(header[:author_thumbnail]).to eq("//yt3.example/first") end - it "falls back to the ucid when the carousel carries no topic details" do + it "raises when the carousel carries no topic details" do initdata = JSON.parse(<<-JSON).as_h { "header": { @@ -82,10 +82,31 @@ Spectator.describe "extract_auto_generated_channel_header" do } JSON - header = extract_auto_generated_channel_header(initdata, "UCEgdi0XIXXZ-qJOFPf4JSKw") + expect do + extract_auto_generated_channel_header(initdata, "UCEgdi0XIXXZ-qJOFPf4JSKw") + end.to raise_error(InfoException) + end - expect(header[:author]).to eq("UCEgdi0XIXXZ-qJOFPf4JSKw") - expect(header[:author_thumbnail]).to eq("") + it "raises when the carousel topic details have no title" do + initdata = JSON.parse(<<-JSON).as_h + { + "header": { + "carouselHeaderRenderer": { + "contents": [ + { + "topicChannelDetailsRenderer": { + "avatar": {"thumbnails": [{"url": "//yt3.example/avatar"}]} + } + } + ] + } + } + } + JSON + + expect do + extract_auto_generated_channel_header(initdata, "UCEgdi0XIXXZ-qJOFPf4JSKw") + end.to raise_error(InfoException) end it "parses the current pageHeaderRenderer shape" do diff --git a/spec/invidious/yt_backend/extractors_spec.cr b/spec/invidious/yt_backend/extractors_spec.cr new file mode 100644 index 00000000..a4c2dc0a --- /dev/null +++ b/spec/invidious/yt_backend/extractors_spec.cr @@ -0,0 +1,22 @@ +require "../../parsers_helper" + +Spectator.describe "YouTubeTabs" do + it "treats a selected tab without content as empty" do + initdata = JSON.parse(<<-JSON).as_h + { + "contents": { + "twoColumnBrowseResultsRenderer": { + "tabs": [ + {"tabRenderer": {"selected": true}} + ] + } + } + } + JSON + + items, continuation = extract_items(initdata) + + expect(items).to be_empty + expect(continuation).to be_nil + end +end diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index aec8d27d..2e027062 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -75,9 +75,15 @@ def extract_auto_generated_channel_header(initdata : Hash(String, JSON::Any), uc # This shape carries neither a banner nor a description. details = extract_topic_channel_details(initdata) - author = details.try &.dig?("title", "simpleText").try &.as_s || ucid + unless details + raise InfoException.new("Could not extract the carousel header of channel #{ucid}") + end + + author = details.dig?("title", "simpleText").try &.as_s + author ||= raise InfoException.new("Could not extract the carousel title of channel #{ucid}") author_url = "https://www.youtube.com/channel/#{ucid}" - author_thumbnail = details.try &.dig?("avatar", "thumbnails", 0, "url").try &.as_s || "" + author_thumbnail = details.dig?("avatar", "thumbnails", 0, "url").try &.as_s + author_thumbnail ||= raise InfoException.new("Could not extract the carousel thumbnail of channel #{ucid}") else raise InfoException.new("Could not extract the header of channel #{ucid}") end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index b2226e74..72c3e144 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -964,7 +964,8 @@ private module Extractors private def self.extract(target) raw_items = [] of JSON::Any - content = extract_selected_tab(target["tabs"])["content"] + content = extract_selected_tab(target["tabs"])["content"]? + return raw_items if content.nil? if section_list_contents = content.dig?("sectionListRenderer", "contents") raw_items = unpack_section_list(section_list_contents)