From 313bfa15a44bcde4adb416cfb9e4689485cedf69 Mon Sep 17 00:00:00 2001 From: Rudra Date: Thu, 6 Aug 2026 02:52:37 +0530 Subject: [PATCH] fix: link multi-creator related videos --- .../videos/related_videos_extract_spec.cr | 101 ++++++++++++++++++ src/invidious/videos/parser.cr | 12 +++ 2 files changed, 113 insertions(+) create mode 100644 spec/invidious/videos/related_videos_extract_spec.cr diff --git a/spec/invidious/videos/related_videos_extract_spec.cr b/spec/invidious/videos/related_videos_extract_spec.cr new file mode 100644 index 000000000..f4d802f36 --- /dev/null +++ b/spec/invidious/videos/related_videos_extract_spec.cr @@ -0,0 +1,101 @@ +require "../../parsers_helper.cr" + +Spectator.describe "parse_related_video" do + it "uses a directly linked channel ID" do + related = JSON.parse(<<-JSON) + { + "videoId": "video-id", + "title": {"simpleText": "Video title"}, + "shortBylineText": { + "runs": [{ + "text": "Channel name", + "navigationEndpoint": { + "browseEndpoint": {"browseId": "UC-direct"} + } + }] + } + } + JSON + + video = Invidious::Videos::Parser.parse_related_video(related).not_nil! + + expect(video["author"]).to eq("Channel name") + expect(video["ucid"]).to eq("UC-direct") + end + + it "links a collaboration byline to the first listed collaborator" do + related = JSON.parse(<<-JSON) + { + "videoId": "collaboration-id", + "title": {"simpleText": "Collaboration"}, + "shortBylineText": { + "runs": [{ + "text": "First Channel and Second Channel", + "navigationEndpoint": { + "showDialogCommand": { + "panelLoadingStrategy": { + "inlineContent": { + "dialogViewModel": { + "customContent": { + "listViewModel": { + "listItems": [ + { + "listItemViewModel": { + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "browseEndpoint": {"browseId": "UC-first"} + } + } + } + } + } + }, + { + "listItemViewModel": { + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "browseEndpoint": {"browseId": "UC-second"} + } + } + } + } + } + } + ] + } + } + } + } + } + } + } + }] + } + } + JSON + + video = Invidious::Videos::Parser.parse_related_video(related).not_nil! + + expect(video["author"]).to eq("First Channel and Second Channel") + expect(video["ucid"]).to eq("UC-first") + end + + it "keeps the channel ID empty when no destination is available" do + related = JSON.parse(<<-JSON) + { + "videoId": "unlinked-id", + "title": {"simpleText": "Unlinked video"}, + "shortBylineText": {"runs": [{"text": "Unlinked author"}]} + } + JSON + + video = Invidious::Videos::Parser.parse_related_video(related).not_nil! + + expect(video["author"]).to eq("Unlinked author") + expect(video["ucid"]).to eq("") + end +end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 8367fcd78..791eb471e 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -28,6 +28,18 @@ module Invidious::Videos::Parser ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) } + # Collaborations use a dialog command instead of linking the combined + # byline directly to a channel. Use the first listed collaborator as the + # destination while preserving YouTube's combined author label. + if ucid.try &.empty? + ucid = channel_info.try &.dig?( + "navigationEndpoint", "showDialogCommand", "panelLoadingStrategy", + "inlineContent", "dialogViewModel", "customContent", "listViewModel", + "listItems", 0, "listItemViewModel", "rendererContext", "commandContext", + "onTap", "innertubeCommand", "browseEndpoint", "browseId" + ).try &.as_s + end + short_view_count = related.try do |r| HelperExtractors.get_short_view_count(r).to_s end