From bcbbda6dcf2455065e7baf164f404f69b3e687a2 Mon Sep 17 00:00:00 2001 From: NDilanka Date: Fri, 21 Aug 2026 22:59:47 +0530 Subject: [PATCH] fix(parser): restore channel link on related videos with multiple creators On collab videos the byline run carries no browseEndpoint, so the ucid came back empty and the watch page rendered no channel link. Fall back to the first entry of the Collaborators dialog embedded in the run's navigationEndpoint. --- .../videos/related_videos_extract_spec.cr | 125 ++++++++++++++++++ src/invidious/videos/parser.cr | 13 ++ 2 files changed, 138 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..71d53eae7 --- /dev/null +++ b/spec/invidious/videos/related_videos_extract_spec.cr @@ -0,0 +1,125 @@ +require "../../parsers_helper.cr" + +Spectator.describe "parse_related_video" do + it "parses a related video from a single-creator channel" do + related = JSON.parse(%({ + "videoId": "BtlWoqWLm9Q", + "title": {"simpleText": "Secret History #4: How Evil Triumphs"}, + "shortBylineText": { + "runs": [ + { + "text": "Predictive History", + "navigationEndpoint": { + "clickTrackingParams": "CAA=", + "commandMetadata": { + "webCommandMetadata": { + "url": "/channel/UC11aHtNnc5bEPLI4jf6mnYg", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UC11aHtNnc5bEPLI4jf6mnYg", + "canonicalBaseUrl": "/@PredictiveHistory" + } + } + } + ] + }, + "lengthInSeconds": 7250, + "shortViewCountText": {"simpleText": "3.2M views"}, + "publishedTimeText": {"simpleText": "11 months ago"} + })) + + related_video = Invidious::Videos::Parser.parse_related_video(related) + + expect(related_video).not_to be_nil + + expect(related_video.not_nil!["author"].as_s).to eq("Predictive History") + expect(related_video.not_nil!["ucid"].as_s).to eq("UC11aHtNnc5bEPLI4jf6mnYg") + expect(related_video.not_nil!["short_view_count"].as_s).to eq("3.2M") + end + + it "parses a related video with multiple creators (collab)" do + related = JSON.parse(%({ + "videoId": "Qifsxitq_q4", + "title": {"simpleText": "Professor Brian Greene: The Threat of AI"}, + "shortBylineText": { + "runs": [ + { + "text": "The Diary Of A CEO and World Science Festival", + "navigationEndpoint": { + "clickTrackingParams": "CAA=", + "showDialogCommand": { + "panelLoadingStrategy": { + "inlineContent": { + "dialogViewModel": { + "customContent": { + "listViewModel": { + "listItems": [ + { + "listItemViewModel": { + "title": {"content": "The Diary Of A CEO"}, + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "clickTrackingParams": "CAA=", + "commandMetadata": { + "webCommandMetadata": { + "url": "/channel/UCGq-a57w-aPwyi3pW7XLiHw", + "webPageType": "WEB_PAGE_TYPE_CHANNEL", + "rootVe": 3611, + "apiUrl": "/youtubei/v1/browse" + } + }, + "browseEndpoint": { + "browseId": "UCGq-a57w-aPwyi3pW7XLiHw" + } + } + } + } + } + } + }, + { + "listItemViewModel": { + "title": {"content": "World Science Festival"}, + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "browseEndpoint": { + "browseId": "UCshBEKIPwFyXClTBFRwBaBA" + } + } + } + } + } + } + } + ] + } + } + } + } + } + } + } + } + ] + }, + "lengthInSeconds": 7400, + "shortViewCountText": {"simpleText": "1M views"}, + "publishedTimeText": {"simpleText": "4 days ago"} + })) + + related_video = Invidious::Videos::Parser.parse_related_video(related) + + expect(related_video).not_to be_nil + + expect(related_video.not_nil!["author"].as_s).to eq("The Diary Of A CEO and World Science Festival") + expect(related_video.not_nil!["ucid"].as_s).to eq("UCGq-a57w-aPwyi3pW7XLiHw") + end +end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 8367fcd78..3b0cc10bf 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -28,6 +28,19 @@ module Invidious::Videos::Parser ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) } + # Videos with multiple creators (collabs) have no browse endpoint on the + # byline run. YouTube instead embeds a "Collaborators" dialog under the + # run's navigation endpoint, where each list item carries the browse + # endpoint of one of the creators. Use the first one as fallback. + 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