fix: resolve channel of related videos published as a collaboration

Related videos published as a collaboration between several channels
carry no "browseEndpoint" in their byline. Their byline opens a
"Collaborators" dialog instead, which holds the channel of each
collaborator, so `get_browse_id` returned an empty string and the
recommendation was rendered without a link to the channel.

Fall back to the first entry of that dialog, which is the channel the
video was published on.

Fixes #5722

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
misinierijon4-debug 2026-08-19 18:43:38 +02:00
parent 821365cf71
commit f708bda6e4
3 changed files with 125 additions and 0 deletions

View File

@ -0,0 +1,103 @@
require "../../parsers_helper.cr"
Spectator.describe "parse_related_video" do
it "extracts the channel of a video published as a collaboration" do
# A video published as a collaboration between two channels. Its byline
# carries no "browseEndpoint"; it opens a "Collaborators" dialog instead,
# and the channel of each collaborator sits inside that dialog.
#
# See: https://github.com/iv-org/invidious/issues/5722
related = JSON.parse(<<-JSON)
{
"videoId": "dbVdnL6HWOg",
"title": { "simpleText": "Professor Jiang Debates Iran War, Trump And China Ties" },
"lengthInSeconds": 2241,
"shortViewCountText": { "simpleText": "1M views" },
"publishedTimeText": { "simpleText": "3 months ago" },
"shortBylineText": {
"runs": [
{
"text": "Piers Morgan Uncensored and ProfSteveKeen",
"navigationEndpoint": {
"showDialogCommand": {
"panelLoadingStrategy": {
"inlineContent": {
"dialogViewModel": {
"customContent": {
"listViewModel": {
"listItems": [
{
"listItemViewModel": {
"title": { "content": "Piers Morgan Uncensored" },
"rendererContext": {
"commandContext": {
"onTap": {
"innertubeCommand": {
"browseEndpoint": { "browseId": "UCatt7TBjfBkiJWx8khav_Gg" }
}
}
}
}
}
},
{
"listItemViewModel": {
"title": { "content": "ProfSteveKeen" },
"rendererContext": {
"commandContext": {
"onTap": {
"innertubeCommand": {
"browseEndpoint": { "browseId": "UCM1ubsbE-tG9ru61mc3zX8A" }
}
}
}
}
}
}
]
}
}
}
}
}
}
}
}
]
}
}
JSON
parsed = Invidious::Videos::Parser.parse_related_video(related).not_nil!
expect(parsed["ucid"].as_s).to eq("UCatt7TBjfBkiJWx8khav_Gg")
expect(parsed["author"].as_s).to eq("Piers Morgan Uncensored and ProfSteveKeen")
end
it "keeps extracting the channel of a single author video" do
related = JSON.parse(<<-JSON)
{
"videoId": "_g4l7YkDQwA",
"title": { "simpleText": "The Diary Of A CEO episode" },
"lengthInSeconds": 1337,
"shortViewCountText": { "simpleText": "2.1M views" },
"publishedTimeText": { "simpleText": "1 month ago" },
"shortBylineText": {
"runs": [
{
"text": "The Diary Of A CEO",
"navigationEndpoint": {
"browseEndpoint": { "browseId": "UCGq-a57w-aPwyi3pW7XLiHw" }
}
}
]
}
}
JSON
parsed = Invidious::Videos::Parser.parse_related_video(related).not_nil!
expect(parsed["ucid"].as_s).to eq("UCGq-a57w-aPwyi3pW7XLiHw")
expect(parsed["author"].as_s).to eq("The Diary Of A CEO")
end
end

View File

@ -28,6 +28,13 @@ module Invidious::Videos::Parser
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
# Videos published as a collaboration have no "browseEndpoint" in their
# byline, so the channel has to be read from the "Collaborators" dialog
# that the byline opens instead. See `get_collaborator_browse_id`.
if ucid.nil? || ucid.empty?
ucid = channel_info.try { |ci| HelperExtractors.get_collaborator_browse_id(ci) }
end
short_view_count = related.try do |r|
HelperExtractors.get_short_view_count(r).to_s
end

View File

@ -1143,6 +1143,21 @@ module HelperExtractors
def self.get_browse_id(container)
return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
end
# Videos published as a collaboration have no "browseEndpoint" in their
# byline. Their byline opens a "Collaborators" dialog instead, which holds
# the channel of every collaborator. Retrieves the ID of the first entry,
# which is the channel the video was published on.
#
# Returns an empty string when it's unable to do so
def self.get_collaborator_browse_id(container)
return container.dig?(
"navigationEndpoint", "showDialogCommand", "panelLoadingStrategy",
"inlineContent", "dialogViewModel", "customContent", "listViewModel",
"listItems", 0, "listItemViewModel", "rendererContext", "commandContext",
"onTap", "innertubeCommand", "browseEndpoint", "browseId"
).try &.as_s || ""
end
end
# Parses an item from Youtube's JSON response into a more usable structure.