mirror of
https://github.com/iv-org/invidious.git
synced 2026-09-06 09:02:45 -05:00
Merge 7f990b061a1b0ade750219310b1b47b1d9c623a7 into 049d591d294e2a43cb32b97a0bb018c2093e5beb
This commit is contained in:
commit
30fce1ca21
68
spec/invidious/videos/captions_matching_spec.cr
Normal file
68
spec/invidious/videos/captions_matching_spec.cr
Normal 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
|
||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 = {
|
||||||
"",
|
"",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user