Files
arr/crates/arr-probe/src/language.rs
T
naps62-yolo 1cb7f1b2fa
ci / rust (push) Successful in 1m20s
ci / web (push) Successful in 7s
e2e / e2e (push) Successful in 1m28s
feat(probe): ffprobe wrapper (#59)
2026-08-22 20:52:09 +01:00

146 lines
3.9 KiB
Rust

//! Turning a track's language tag into an [`arr_core::Language`].
//!
//! The Portuguese case is not decided here. `arr-core::lang` owns the §5.2
//! signal order, and this module's job is to hand it the two signals a file
//! carries — the stream `title` and `handler_name` strings, and the
//! container's BCP-47 tag. The third signal, release-name markers, belongs to
//! the release and is deliberately not consulted from a file probe.
use arr_core::{
lang::{resolve_portuguese, PortugueseEvidence},
Language,
};
/// ISO-639-2 to ISO-639-1, for the codes `ffprobe` actually emits.
///
/// TMDB reports a title's original language as ISO-639-1 (`en`), `ffprobe`
/// reports a track's as ISO-639-2/B (`eng`). The §5.2 rule compares the two,
/// so they have to be spelled the same way by the time they meet.
const ISO_639_2_TO_1: [(&str, &str); 40] = [
("ara", "ar"),
("bul", "bg"),
("cat", "ca"),
("ces", "cs"),
("chi", "zh"),
("cze", "cs"),
("dan", "da"),
("deu", "de"),
("dut", "nl"),
("ell", "el"),
("eng", "en"),
("fin", "fi"),
("fra", "fr"),
("fre", "fr"),
("ger", "de"),
("gle", "ga"),
("heb", "he"),
("hin", "hi"),
("hun", "hu"),
("ind", "id"),
("isl", "is"),
("ita", "it"),
("jpn", "ja"),
("kor", "ko"),
("nld", "nl"),
("nor", "no"),
("pol", "pl"),
("ron", "ro"),
("rum", "ro"),
("rus", "ru"),
("slk", "sk"),
("slo", "sk"),
("spa", "es"),
("swe", "sv"),
("tha", "th"),
("tur", "tr"),
("ukr", "uk"),
("und", "und"),
("vie", "vi"),
("zho", "zh"),
];
/// Resolve one track's language from its tag and its self-description.
pub(crate) fn resolve(tag: Option<&str>, title: Option<&str>, handler: Option<&str>) -> Language {
let normalised = tag.unwrap_or("und").trim().to_ascii_lowercase();
let primary = normalised
.split(['-', '_'])
.next()
.unwrap_or(normalised.as_str());
if matches!(primary, "pt" | "por") {
return resolve_portuguese(PortugueseEvidence {
// Signal 1 is a property of the release, not of the file.
name_markers: &[],
stream_title: title,
handler_name: handler,
container_tag: tag,
});
}
let code = ISO_639_2_TO_1
.iter()
.find(|(long, _)| *long == primary)
.map_or(primary, |(_, short)| *short);
Language::Other(code.to_owned())
}
#[cfg(test)]
mod tests {
use arr_core::Language;
use super::resolve;
#[test]
fn iso_639_2_becomes_iso_639_1() {
assert_eq!(
resolve(Some("eng"), None, None),
Language::Other("en".to_owned())
);
assert_eq!(
resolve(Some("ger"), None, None),
Language::Other("de".to_owned())
);
}
#[test]
fn an_untagged_track_is_undetermined_not_portuguese() {
assert_eq!(resolve(None, None, None), Language::Other("und".to_owned()));
}
#[test]
fn bcp_47_region_decides_on_its_own() {
assert_eq!(
resolve(Some("pt-BR"), None, None),
Language::PortugueseBrazil
);
assert_eq!(
resolve(Some("pt-PT"), None, None),
Language::PortuguesePortugal
);
}
#[test]
fn the_track_title_decides_when_the_code_cannot() {
assert_eq!(
resolve(Some("por"), Some("Portuguese (Brazil)"), None),
Language::PortugueseBrazil
);
assert_eq!(
resolve(Some("por"), None, Some("Português (Portugal)")),
Language::PortuguesePortugal
);
}
#[test]
fn plain_portuguese_stays_unverified() {
assert_eq!(
resolve(Some("por"), Some("Português"), Some("SoundHandler")),
Language::PortugueseUnverified
);
assert_eq!(
resolve(Some("pt"), None, None),
Language::PortugueseUnverified
);
}
}