From a4d0a195cde35b93a8d9292498e71c3ab6a74325 Mon Sep 17 00:00:00 2001 From: XZH Date: Sat, 5 Sep 2026 00:02:42 -0700 Subject: [PATCH] fix(captions): match preferred language to auto-generated tracks Preferred caption slots use names such as English, but many videos only expose English (auto-generated) with language code en. Exact name matching left those tracks unordered, so the player could auto-select another auto-generated language. Match preference names to regional and auto-generated variants, keep human tracks ahead of auto-generated ones, and share the matcher on watch and embed. Fixes #6007 --- .../videos/captions_matching_spec.cr | 36 ++++++++++++++++ src/invidious/routes/embed.cr | 9 +--- src/invidious/routes/watch.cr | 9 +--- src/invidious/videos/caption.cr | 43 +++++++++++++++++++ 4 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 spec/invidious/videos/captions_matching_spec.cr diff --git a/spec/invidious/videos/captions_matching_spec.cr b/spec/invidious/videos/captions_matching_spec.cr new file mode 100644 index 000000000..d2cbb0730 --- /dev/null +++ b/spec/invidious/videos/captions_matching_spec.cr @@ -0,0 +1,36 @@ +require "../../spec_helper" + +Spectator.describe Invidious::Videos::Captions do + describe ".matching" do + it "matches English to English auto-generated ahead of other auto tracks" do + arabic = Invidious::Videos::Captions::Metadata.new("Arabic (auto-generated)", "ar", "http://x", true) + english_auto = Invidious::Videos::Captions::Metadata.new("English (auto-generated)", "en", "http://x", true) + + matched = Invidious::Videos::Captions.matching([arabic, english_auto], ["English"]) + expect(matched.map(&.name)).to eq(["English (auto-generated)"]) + end + + it "prefers a human English track over auto-generated English" do + english_auto = Invidious::Videos::Captions::Metadata.new("English (auto-generated)", "en", "http://x", true) + english = Invidious::Videos::Captions::Metadata.new("English", "en", "http://x", false) + + matched = Invidious::Videos::Captions.matching([english_auto, english], ["English"]) + expect(matched.map(&.name)).to eq(["English", "English (auto-generated)"]) + end + + it "matches regional English names and language codes" do + us = Invidious::Videos::Captions::Metadata.new("English (United States)", "en-US", "http://x", false) + uk = Invidious::Videos::Captions::Metadata.new("English (United Kingdom)", "en-GB", "http://x", false) + german = Invidious::Videos::Captions::Metadata.new("German (Germany)", "de-DE", "http://x", false) + + matched = Invidious::Videos::Captions.matching([german, us, uk], ["English", "en"]) + expect(matched.map(&.name)).to eq(["English (United States)", "English (United Kingdom)"]) + end + + it "ignores blank preference slots" do + arabic = Invidious::Videos::Captions::Metadata.new("Arabic (auto-generated)", "ar", "http://x", true) + matched = Invidious::Videos::Captions.matching([arabic], ["", "", ""]) + expect(matched).to be_empty + end + end +end diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr index 41dc517c2..bde4f2271 100644 --- a/src/invidious/routes/embed.cr +++ b/src/invidious/routes/embed.cr @@ -182,14 +182,7 @@ module Invidious::Routes::Embed captions = video.captions - preferred_captions = captions.select { |caption| - params.preferred_captions.includes?(caption.name) || - params.preferred_captions.includes?(caption.language_code.split("-")[0]) - } - preferred_captions.sort_by! { |caption| - (params.preferred_captions.index(caption.name) || - params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil! - } + preferred_captions = Invidious::Videos::Captions.matching(captions, params.preferred_captions) captions = captions - preferred_captions aspect_ratio = nil diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 3c66f1bc0..f26bc6119 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -149,14 +149,7 @@ module Invidious::Routes::Watch captions = video.captions - preferred_captions = captions.select { |caption| - params.preferred_captions.includes?(caption.name) || - params.preferred_captions.includes?(caption.language_code.split("-")[0]) - } - preferred_captions.sort_by! { |caption| - (params.preferred_captions.index(caption.name) || - params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil! - } + preferred_captions = Invidious::Videos::Captions.matching(captions, params.preferred_captions) captions = captions - preferred_captions aspect_ratio = "16:9" diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index c811cfe1b..c6c7113d2 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -86,6 +86,49 @@ module Invidious::Videos end end + # Tracks whose name or language matches the user's caption preferences, + # ranked by preference slot then human tracks before auto-generated. + def self.matching(captions : Array(Metadata), names : Array(String)) : Array(Metadata) + wanted = names.map(&.strip).reject(&.empty?) + return [] of Metadata if wanted.empty? + + selected = captions.select { |caption| matches?(caption, wanted) } + selected.sort_by! { |caption| rank(caption, wanted) } + selected + end + + def self.matches?(caption : Metadata, names : Array(String)) : Bool + names.any? { |name| name_matches?(caption, name) } + end + + private def self.name_matches?(caption : Metadata, name : String) : Bool + needle = name.strip.downcase + return false if needle.empty? + + caption_name = caption.name.downcase + lang = caption.language_code.downcase + base_lang = lang.split("-")[0] + + caption_name == needle || + caption_name.starts_with?(needle + " (") || + caption_name.starts_with?(needle + " - ") || + lang == needle || + base_lang == needle + end + + private def self.rank(caption : Metadata, names : Array(String)) : Tuple(Int32, Int32) + pref_rank = names.size + names.each_with_index do |name, index| + if name_matches?(caption, name) + pref_rank = index + break + end + end + + auto_rank = caption.auto_generated ? 1 : 0 + {pref_rank, auto_rank} + end + # List of all caption languages available on Youtube. LANGUAGES = { "",