diff --git a/spec/invidious/videos/parser_spec.cr b/spec/invidious/videos/parser_spec.cr new file mode 100644 index 000000000..164358e0a --- /dev/null +++ b/spec/invidious/videos/parser_spec.cr @@ -0,0 +1,74 @@ +require "../../parsers_helper" + +Spectator.describe Invidious::Videos::Parser do + describe ".parse_related_video" do + it "uses the first creator channel for a collaborative byline" do + related = JSON.parse(%( + { + "videoId": "BTJGr78-zyw", + "title": { + "simpleText": "Collaborative video" + }, + "shortBylineText": { + "runs": [ + { + "text": "The Diary Of A CEO and Predictive History", + "navigationEndpoint": { + "showDialogCommand": { + "panelLoadingStrategy": { + "inlineContent": { + "dialogViewModel": { + "customContent": { + "listViewModel": { + "listItems": [ + { + "listItemViewModel": { + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "browseEndpoint": { + "browseId": "UCGq-a57w-aPwyi3pW7XLiHw" + } + } + } + } + } + } + }, + { + "listItemViewModel": { + "rendererContext": { + "commandContext": { + "onTap": { + "innertubeCommand": { + "browseEndpoint": { + "browseId": "UC11aHtNnc5bEPLI4jf6mnYg" + } + } + } + } + } + } + } + ] + } + } + } + } + } + } + } + } + ] + } + } + )) + + parsed = Invidious::Videos::Parser.parse_related_video(related).not_nil! + + expect(parsed["author"].as_s).to eq("The Diary Of A CEO and Predictive History") + expect(parsed["ucid"].as_s).to eq("UCGq-a57w-aPwyi3pW7XLiHw") + end + end +end diff --git a/spec/invidious/yt_backend/extractors_spec.cr b/spec/invidious/yt_backend/extractors_spec.cr new file mode 100644 index 000000000..951e3ad96 --- /dev/null +++ b/spec/invidious/yt_backend/extractors_spec.cr @@ -0,0 +1,25 @@ +require "../../parsers_helper" + +Spectator.describe HelperExtractors do + describe ".get_browse_id" do + it "extracts a direct browse endpoint" do + container = JSON.parse(%( + { + "navigationEndpoint": { + "browseEndpoint": { + "browseId": "UCdirect" + } + } + } + )) + + expect(HelperExtractors.get_browse_id(container)).to eq("UCdirect") + end + + it "returns an empty string when no browse endpoint exists" do + container = JSON.parse(%({"navigationEndpoint":{"showDialogCommand":{}}})) + + expect(HelperExtractors.get_browse_id(container)).to be_empty + end + end +end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index b2226e74d..9aeac540a 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -1141,7 +1141,29 @@ module HelperExtractors # Retrieves the ID required for querying the InnerTube browse endpoint. # Returns an empty string when it's unable to do so def self.get_browse_id(container) - return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || "" + browse_id = container.dig?("navigationEndpoint", "browseEndpoint", "browseId") + return browse_id.as_s if browse_id + + # Collaborative bylines open a dialog instead of linking to a channel. + # Use the first listed creator because video items store a single channel. + return container.dig?( + "navigationEndpoint", + "showDialogCommand", + "panelLoadingStrategy", + "inlineContent", + "dialogViewModel", + "customContent", + "listViewModel", + "listItems", + 0, + "listItemViewModel", + "rendererContext", + "commandContext", + "onTap", + "innertubeCommand", + "browseEndpoint", + "browseId" + ).try &.as_s || "" end end