initial commit
Publish image / publish (push) Failing after 14s

This commit is contained in:
Worph
2026-09-12 21:31:35 +02:00
parent d6086a365b
commit deeba3857b
22 changed files with 4528 additions and 1 deletions
@@ -0,0 +1,145 @@
//! OpenSubtitles language codes ↔ the stack's ISO 639-2/B vocabulary.
//!
//! ⚠ **The stack compares 639-2/B (`fre`, `ger`, `chi`), not 639-3 (`fra`).**
//! `meta_feeder_sdk::lang::normalize_lang_code` normalises onto it, torznab
//! writes `languages/fre`, and meta-search's language prefs store `fre`.
//! `languages_filter_matches` compares strings, so a record stamped `fra` is
//! *dropped* by a `languages:fre` query — a concrete, non-matching language.
//! (METADATA_KEYS.md still says 639-3 for `languages/*`; the code is what bites.)
//!
//! OpenSubtitles speaks its own dialect: mostly 639-1, plus region tags
//! (`pt-br`, `zh-tw`) and a few private codes (`ze` Chinese bilingual, `sp`/`ea`
//! Spanish EU/LA, `pm` Portuguese MZ). Responses use mixed case (`pt-BR`).
use meta_feeder_sdk::lang::normalize_lang_code;
/// Every `language_code` from `GET /api/v1/infos/languages` (105 entries,
/// fetched 2026-09-12) → ISO 639-2/B. Codes with no 639-2 entry use 639-3
/// (`ast`, `ext`, `cnr`, `tok`), which `normalize_lang_code` passes through.
const OS_TO_639_2B: &[(&str, &str)] = &[
("ab", "abk"), ("af", "afr"), ("sq", "alb"), ("am", "amh"), ("ar", "ara"),
("an", "arg"), ("hy", "arm"), ("as", "asm"), ("at", "ast"), ("az-az", "aze"),
("eu", "baq"), ("be", "bel"), ("bn", "ben"), ("bs", "bos"), ("br", "bre"),
("bg", "bul"), ("my", "bur"), ("ca", "cat"), ("ze", "chi"), ("zh-ca", "chi"),
("zh-cn", "chi"), ("zh-tw", "chi"), ("hr", "hrv"), ("cs", "cze"), ("da", "dan"),
("pr", "per"), ("nl", "dut"), ("en", "eng"), ("eo", "epo"), ("et", "est"),
("ex", "ext"), ("fi", "fin"), ("fr", "fre"), ("gd", "gla"), ("gl", "glg"),
("ka", "geo"), ("de", "ger"), ("el", "gre"), ("he", "heb"), ("hi", "hin"),
("hu", "hun"), ("is", "ice"), ("ig", "ibo"), ("id", "ind"), ("ia", "ina"),
("ga", "gle"), ("it", "ita"), ("ja", "jpn"), ("kn", "kan"), ("kk", "kaz"),
("km", "khm"), ("ko", "kor"), ("ku", "kur"), ("lv", "lav"), ("lt", "lit"),
("lb", "ltz"), ("mk", "mac"), ("ms", "may"), ("ml", "mal"), ("ma", "mni"),
("mr", "mar"), ("mn", "mon"), ("me", "cnr"), ("nv", "nav"), ("ne", "nep"),
("se", "sme"), ("no", "nor"), ("oc", "oci"), ("or", "ori"), ("fa", "per"),
("pl", "pol"), ("pt-pt", "por"), ("pt-br", "por"), ("pm", "por"), ("ps", "pus"),
("ro", "rum"), ("ru", "rus"), ("sx", "sat"), ("sr", "srp"), ("sd", "snd"),
("si", "sin"), ("sk", "slo"), ("sl", "slv"), ("so", "som"), ("az-zb", "aze"),
("es", "spa"), ("sp", "spa"), ("ea", "spa"), ("sw", "swa"), ("sv", "swe"),
("sy", "syr"), ("tl", "tgl"), ("ta", "tam"), ("tt", "tat"), ("te", "tel"),
("tm-td", "tet"), ("th", "tha"), ("tp", "tok"), ("tr", "tur"), ("tk", "tuk"),
("uk", "ukr"), ("ur", "urd"), ("uz", "uzb"), ("vi", "vie"), ("cy", "wel"),
];
/// 639-2/T → 639-2/B for the table's codes that `normalize_lang_code` does not
/// already fold (it covers `fra`/`deu`/`zho`/`nld`/`ell`/`ron`/`slk`/`ces`).
const T_TO_B: &[(&str, &str)] = &[
("sqi", "alb"), ("hye", "arm"), ("eus", "baq"), ("mya", "bur"), ("kat", "geo"),
("isl", "ice"), ("mkd", "mac"), ("msa", "may"), ("fas", "per"), ("cym", "wel"),
];
/// Canonical 639-2/B for any spelling a query or response might carry: 639-2/B,
/// 639-2/T, 639-1, or an OpenSubtitles code. `None` when nothing maps.
fn canonical(code: &str) -> Option<String> {
let c = code.trim().to_ascii_lowercase();
if c.is_empty() {
return None;
}
if let Some((_, b)) = T_TO_B.iter().find(|(t, _)| *t == c) {
return Some((*b).to_string());
}
if let Some((_, b)) = OS_TO_639_2B.iter().find(|(os, _)| *os == c) {
return Some((*b).to_string());
}
let n = normalize_lang_code(&c);
if n.len() == 3 && n.bytes().all(|b| b.is_ascii_lowercase()) && n != "und" {
return Some(n);
}
// A region-tagged spelling the table does not list (`en-us`): try the
// primary subtag once.
let primary = c.split(['-', '_']).next().unwrap_or("");
if primary != c && !primary.is_empty() {
return canonical(primary);
}
None
}
/// A subtitle's `language` from a search response → the code stamped on the
/// record (`subtitleLanguage`, `languages/<code>`). `None` for unknown input:
/// METADATA_KEYS says `und` is never written — an unknown language is simply
/// absent, which `languages:` filters treat as "may match".
pub fn from_os_code(code: &str) -> Option<String> {
canonical(code)
}
/// A query's `languages:` value → every OpenSubtitles code that denotes it,
/// sorted (`por` → `pm`, `pt-br`, `pt-pt`). Empty when the language is unknown
/// to OpenSubtitles.
pub fn to_os_codes(lang: &str) -> Vec<&'static str> {
let Some(b) = canonical(lang) else {
return Vec::new();
};
let mut codes: Vec<&'static str> = OS_TO_639_2B
.iter()
.filter(|(_, x)| *x == b)
.map(|(os, _)| *os)
.collect();
codes.sort_unstable();
codes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_is_the_full_opensubtitles_list_with_unique_codes() {
assert_eq!(OS_TO_639_2B.len(), 105);
let mut os: Vec<_> = OS_TO_639_2B.iter().map(|(o, _)| *o).collect();
os.sort_unstable();
os.dedup();
assert_eq!(os.len(), 105);
}
/// Every code the table emits must survive the SDK normaliser unchanged,
/// or records and `languages:` filters would disagree.
#[test]
fn emitted_codes_are_fixed_points_of_the_sdk_normaliser() {
for (_, b) in OS_TO_639_2B {
assert_eq!(normalize_lang_code(b), *b, "{b}");
assert_eq!(canonical(b).as_deref(), Some(*b), "{b}");
}
}
#[test]
fn response_codes_map_to_639_2b() {
assert_eq!(from_os_code("fr").as_deref(), Some("fre"));
assert_eq!(from_os_code("pt-BR").as_deref(), Some("por"));
assert_eq!(from_os_code("zh-TW").as_deref(), Some("chi"));
assert_eq!(from_os_code("fa").as_deref(), Some("per"));
assert_eq!(from_os_code("en-US").as_deref(), Some("eng"));
assert_eq!(from_os_code(""), None);
assert_eq!(from_os_code("xx"), None);
assert_eq!(from_os_code("und"), None);
}
#[test]
fn query_codes_accept_every_spelling() {
assert_eq!(to_os_codes("fre"), vec!["fr"]);
assert_eq!(to_os_codes("fra"), vec!["fr"]);
assert_eq!(to_os_codes("fr"), vec!["fr"]);
assert_eq!(to_os_codes("FAS"), vec!["fa", "pr"]);
assert_eq!(to_os_codes("por"), vec!["pm", "pt-br", "pt-pt"]);
assert_eq!(to_os_codes("spa"), vec!["ea", "es", "sp"]);
assert!(to_os_codes("zzz").is_empty());
}
}