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
This commit is contained in:
XZH 2026-09-05 00:02:42 -07:00
parent 049d591d29
commit a4d0a195cd
4 changed files with 81 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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 = {
"",