Merge 7f990b061a1b0ade750219310b1b47b1d9c623a7 into 049d591d294e2a43cb32b97a0bb018c2093e5beb

This commit is contained in:
evan188199-tech 2026-09-05 07:41:51 +00:00 committed by GitHub
commit 30fce1ca21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 196 additions and 16 deletions

View File

@ -0,0 +1,68 @@
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 "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 "prefers en-US over en-GB for English (United States)" do
us = Invidious::Videos::Captions::Metadata.new("en-US", "en-US", "http://x", false)
uk = Invidious::Videos::Captions::Metadata.new("en-GB", "en-GB", "http://x", false)
matched = Invidious::Videos::Captions.matching([uk, us], ["English (United States)"])
expect(matched.map(&.name)).to eq(["en-US", "en-GB"])
end
it "prefers human base-language English over exact regional auto-generated" do
human = Invidious::Videos::Captions::Metadata.new("English", "en", "http://x", false)
auto_us = Invidious::Videos::Captions::Metadata.new("en-US", "en-US", "http://x", true)
matched = Invidious::Videos::Captions.matching([auto_us, human], ["English (United States)"])
expect(matched.map(&.name)).to eq(["English", "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], ["", "", ""])
expect(matched).to be_empty
end
end
end

View File

@ -182,14 +182,7 @@ module Invidious::Routes::Embed
captions = video.captions captions = video.captions
preferred_captions = captions.select { |caption| preferred_captions = Invidious::Videos::Captions.matching(captions, params.preferred_captions)
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!
}
captions = captions - preferred_captions captions = captions - preferred_captions
aspect_ratio = nil aspect_ratio = nil

View File

@ -149,14 +149,7 @@ module Invidious::Routes::Watch
captions = video.captions captions = video.captions
preferred_captions = captions.select { |caption| preferred_captions = Invidious::Videos::Captions.matching(captions, params.preferred_captions)
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!
}
captions = captions - preferred_captions captions = captions - preferred_captions
aspect_ratio = "16:9" aspect_ratio = "16:9"

View File

@ -86,6 +86,132 @@ module Invidious::Videos
end end
end end
# Tracks whose name or language matches the user's caption preferences,
# ranked by preference slot, then human tracks before auto-generated,
# then more-specific language matches ahead of base-language fallbacks.
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
# Common LANGUAGES names mapped to ISO 639-1 / BCP-47 codes.
# Auto-generated suffixes are stripped before lookup; regional
# qualifiers keep their more specific code and fall back to the base.
NAME_TO_CODE = {
"arabic" => "ar",
"chinese" => "zh",
"chinese (china)" => "zh-cn",
"chinese (hong kong)" => "zh-hk",
"chinese (simplified)" => "zh-hans",
"chinese (taiwan)" => "zh-tw",
"chinese (traditional)" => "zh-hant",
"dutch" => "nl",
"english" => "en",
"english (united kingdom)" => "en-gb",
"english (united states)" => "en-us",
"filipino" => "fil",
"french" => "fr",
"german" => "de",
"indonesian" => "id",
"italian" => "it",
"japanese" => "ja",
"korean" => "ko",
"portuguese" => "pt",
"portuguese (brazil)" => "pt-br",
"russian" => "ru",
"spanish" => "es",
"spanish (latin america)" => "es-419",
"spanish (mexico)" => "es-mx",
"spanish (spain)" => "es-es",
"turkish" => "tr",
"vietnamese" => "vi",
}
private def self.name_matches?(caption : Metadata, name : String) : Bool
!match_index(caption, name).nil?
end
# Tokens from most specific to least: "English (United States)" ->
# "english (united states)", "en-us", "english", "en".
private def self.match_tokens(name : String) : Array(String)
token = name.strip.downcase
return [] of String if token.empty?
lookup = token
if lookup.ends_with?(" (auto-generated)")
lookup = lookup.rchop(" (auto-generated)")
end
tokens = [lookup]
if mapped = NAME_TO_CODE[lookup]?
tokens << mapped
end
base_name = lookup.split(" - ")[0].split(" (")[0].strip
if !base_name.empty? && base_name != lookup
tokens << base_name
if mapped = NAME_TO_CODE[base_name]?
tokens << mapped
end
end
if lookup.includes?("-")
tokens << lookup.split("-", 2)[0]
elsif lookup.size.in?(2..3) && lookup.chars.all?(&.ascii_letter?)
tokens << lookup
end
tokens.uniq
end
private def self.match_index(caption : Metadata, name : String) : Int32?
needles = match_tokens(name)
return nil if needles.empty?
caption_name = caption.name.strip.downcase
lang = caption.language_code.strip.downcase
base_lang = lang.split("-", 2)[0]
caption_tokens = match_tokens(caption.name)
caption_tokens << lang
caption_tokens << base_lang
needles.each_with_index do |needle, index|
if caption_name == needle ||
caption_name.starts_with?(needle + " (") ||
caption_name.starts_with?(needle + " - ") ||
lang == needle ||
(base_lang == needle && !needle.includes?("-")) ||
caption_tokens.includes?(needle)
return index
end
end
nil
end
private def self.rank(caption : Metadata, names : Array(String)) : Tuple(Int32, Int32, Int32)
pref_rank = names.size
specificity = Int32::MAX
names.each_with_index do |name, index|
if found = match_index(caption, name)
pref_rank = index
specificity = found
break
end
end
auto_rank = caption.auto_generated ? 1 : 0
{pref_rank, auto_rank, specificity}
end
# List of all caption languages available on Youtube. # List of all caption languages available on Youtube.
LANGUAGES = { LANGUAGES = {
"", "",