From 3df2266294be28712912d1877bbc09c0482e794a Mon Sep 17 00:00:00 2001 From: XZH Date: Sat, 5 Sep 2026 00:23:58 -0700 Subject: [PATCH] fix(captions): map display-language preferences to language codes Canonicalize names like English to en before matching so code-labeled tracks such as en and en-US honor the preferred caption language. --- .../videos/captions_matching_spec.cr | 17 ++ src/invidious/videos/caption.cr | 151 +++++++++++++++++- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/spec/invidious/videos/captions_matching_spec.cr b/spec/invidious/videos/captions_matching_spec.cr index d2cbb0730..b8f61a12c 100644 --- a/spec/invidious/videos/captions_matching_spec.cr +++ b/spec/invidious/videos/captions_matching_spec.cr @@ -27,6 +27,23 @@ Spectator.describe Invidious::Videos::Captions do expect(matched.map(&.name)).to eq(["English (United States)", "English (United Kingdom)"]) end + + it "matches English preference to a code-labeled en track" do + code_only = Invidious::Videos::Captions::Metadata.new("en", "en", "http://x", true) + arabic = Invidious::Videos::Captions::Metadata.new("ar", "ar", "http://x", true) + + matched = Invidious::Videos::Captions.matching([arabic, code_only], ["English"]) + expect(matched.map(&.name)).to eq(["en"]) + end + + it "matches English preference to a code-labeled en-US track" do + regional = Invidious::Videos::Captions::Metadata.new("en-US", "en-US", "http://x", false) + german = Invidious::Videos::Captions::Metadata.new("de-DE", "de-DE", "http://x", false) + + matched = Invidious::Videos::Captions.matching([german, regional], ["English"]) + expect(matched.map(&.name)).to eq(["en-US"]) + 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], ["", "", ""]) diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index c6c7113d2..ba67e2de6 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -101,6 +101,117 @@ module Invidious::Videos names.any? { |name| name_matches?(caption, name) } end + # Map display-language preferences (e.g. "English") onto ISO-ish codes so + # they can match caption names/codes like "en" or "en-US". + NAME_TO_CODE = { + "afrikaans" => "af", + "albanian" => "sq", + "amharic" => "am", + "arabic" => "ar", + "armenian" => "hy", + "azerbaijani" => "az", + "bangla" => "bn", + "basque" => "eu", + "belarusian" => "be", + "bosnian" => "bs", + "bulgarian" => "bg", + "burmese" => "my", + "cantonese" => "yue", + "catalan" => "ca", + "cebuano" => "ceb", + "chinese" => "zh", + "corsican" => "co", + "croatian" => "hr", + "czech" => "cs", + "danish" => "da", + "dutch" => "nl", + "english" => "en", + "esperanto" => "eo", + "estonian" => "et", + "filipino" => "fil", + "finnish" => "fi", + "french" => "fr", + "galician" => "gl", + "georgian" => "ka", + "german" => "de", + "greek" => "el", + "gujarati" => "gu", + "haitian creole" => "ht", + "hausa" => "ha", + "hawaiian" => "haw", + "hebrew" => "he", + "hindi" => "hi", + "hmong" => "hmn", + "hungarian" => "hu", + "icelandic" => "is", + "igbo" => "ig", + "indonesian" => "id", + "interlingue" => "ie", + "irish" => "ga", + "italian" => "it", + "japanese" => "ja", + "javanese" => "jv", + "kannada" => "kn", + "kazakh" => "kk", + "khmer" => "km", + "korean" => "ko", + "kurdish" => "ku", + "kyrgyz" => "ky", + "lao" => "lo", + "latin" => "la", + "latvian" => "lv", + "lithuanian" => "lt", + "luxembourgish" => "lb", + "macedonian" => "mk", + "malagasy" => "mg", + "malay" => "ms", + "malayalam" => "ml", + "maltese" => "mt", + "maori" => "mi", + "marathi" => "mr", + "mongolian" => "mn", + "nepali" => "ne", + "norwegian bokmål" => "nb", + "norwegian bokmal" => "nb", + "nyanja" => "ny", + "pashto" => "ps", + "persian" => "fa", + "polish" => "pl", + "portuguese" => "pt", + "punjabi" => "pa", + "romanian" => "ro", + "russian" => "ru", + "samoan" => "sm", + "scottish gaelic" => "gd", + "serbian" => "sr", + "shona" => "sn", + "sindhi" => "sd", + "sinhala" => "si", + "slovak" => "sk", + "slovenian" => "sl", + "somali" => "so", + "southern sotho" => "st", + "spanish" => "es", + "sundanese" => "su", + "swahili" => "sw", + "swedish" => "sv", + "tajik" => "tg", + "tamil" => "ta", + "telugu" => "te", + "thai" => "th", + "turkish" => "tr", + "ukrainian" => "uk", + "urdu" => "ur", + "uzbek" => "uz", + "vietnamese" => "vi", + "welsh" => "cy", + "western frisian" => "fy", + "xhosa" => "xh", + "yiddish" => "yi", + "yoruba" => "yo", + "zulu" => "zu", + } + private def self.name_matches?(caption : Metadata, name : String) : Bool needle = name.strip.downcase return false if needle.empty? @@ -109,11 +220,41 @@ module Invidious::Videos 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 + return true if caption_name == needle || + caption_name.starts_with?(needle + " (") || + caption_name.starts_with?(needle + " - ") || + lang == needle || + base_lang == needle + + # Display-language preferences such as "English" must also match + # code-labeled tracks like "en" / "en-US". + needle_codes = canonical_codes(needle) + caption_codes = canonical_codes(caption_name) + caption_codes << lang + caption_codes << base_lang + needle_codes.any? { |code| caption_codes.includes?(code) } + end + + private def self.canonical_codes(value : String) : Array(String) + raw = value.strip.downcase + return [] of String if raw.empty? + + codes = [] of String + base_name = raw.split(" - ")[0].split(" (")[0].strip + + if mapped = NAME_TO_CODE[base_name]? + codes << mapped + end + + if raw.includes?("-") + codes << raw + codes << raw.split("-")[0] + elsif raw.size.in?(2..3) && raw.chars.all?(&.ascii_letter?) + codes << raw + end + + codes.uniq! + codes end private def self.rank(caption : Metadata, names : Array(String)) : Tuple(Int32, Int32)