From 60f7ddda2acf55309578e6f51be7753fe29c53f8 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Sat, 22 Aug 2026 20:45:54 +0100 Subject: [PATCH] refactor(probe): use arr-core for pt-PT versus pt-BR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #57 landed the §5.2 signal order in arr-core::lang while this branch was open. arr-probe now feeds it the two signals a file carries — stream strings and the container's BCP-47 tag — instead of keeping a second copy of the rule. Co-Authored-By: Claude Opus 5 --- crates/arr-probe/src/language.rs | 101 +++++++++---------------------- 1 file changed, 30 insertions(+), 71 deletions(-) diff --git a/crates/arr-probe/src/language.rs b/crates/arr-probe/src/language.rs index ecd66a2..fa00bd7 100644 --- a/crates/arr-probe/src/language.rs +++ b/crates/arr-probe/src/language.rs @@ -1,28 +1,15 @@ //! Turning a track's language tag into an [`arr_core::Language`]. //! -//! The interesting case is Portuguese. ISO-639-2 has one code, `por`, for both -//! variants, so `ffprobe` reports `por` for a Lisbon dub and a São Paulo one -//! alike. DESIGN.md §5.2 lists three signals; two of them are visible here — -//! the stream `title` and `handler_name` strings, and container-level BCP-47 -//! tags. The third, release-name markers, belongs to `arr-parse` and is -//! deliberately not consulted from a file probe. -//! -//! When nothing resolves it the track is [`Language::PortugueseUnverified`]. -//! Guessing is worse than not knowing: guessing towards `kids` hands a child a -//! Brazilian dub, guessing towards `main` rejects a Brazilian film's own audio. +//! 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::Language; - -/// Strings a Brazilian track labels itself with. -const BRAZIL_MARKERS: [&str; 8] = [ - "brazil", "brasil", "pt-br", "pt_br", "ptbr", "dublado", "dublagem", "nacional", -]; - -/// Strings a European Portuguese track labels itself with. `europeu` and -/// `european` only ever appear here to distinguish from Brazilian. -const PORTUGAL_MARKERS: [&str; 7] = [ - "portugal", "pt-pt", "pt_pt", "ptpt", "europeu", "europeia", "european", -]; +use arr_core::{ + lang::{resolve_portuguese, PortugueseEvidence}, + Language, +}; /// ISO-639-2 to ISO-639-1, for the codes `ffprobe` actually emits. /// @@ -34,6 +21,7 @@ const ISO_639_2_TO_1: [(&str, &str); 40] = [ ("bul", "bg"), ("cat", "ca"), ("ces", "cs"), + ("chi", "zh"), ("cze", "cs"), ("dan", "da"), ("deu", "de"), @@ -66,59 +54,34 @@ const ISO_639_2_TO_1: [(&str, &str); 40] = [ ("tha", "th"), ("tur", "tr"), ("ukr", "uk"), + ("und", "und"), ("vie", "vi"), ("zho", "zh"), - ("chi", "zh"), - ("und", "und"), ]; /// 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 tag = tag.unwrap_or("und").trim().to_ascii_lowercase(); - let (primary, region) = match tag.split_once(['-', '_']) { - Some((primary, rest)) => (primary, rest.split(['-', '_']).next().unwrap_or("")), - None => (tag.as_str(), ""), - }; + 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") { - let code = ISO_639_2_TO_1 - .iter() - .find(|(long, _)| *long == primary) - .map_or(primary, |(_, short)| *short); - return Language::Other(code.to_owned()); + 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, + }); } - match region { - "br" => return Language::PortugueseBrazil, - "pt" => return Language::PortuguesePortugal, - _ => {} - } - - from_description(title, handler) -} - -/// Signal two: what the track calls itself, which is usually literally -/// `Portuguese (Brazil)` when it is one. -fn from_description(title: Option<&str>, handler: Option<&str>) -> Language { - let described = [title, handler] - .into_iter() - .flatten() - .map(str::to_ascii_lowercase) - .collect::>(); - - let contains = |markers: &[&str]| { - described - .iter() - .any(|text| markers.iter().any(|marker| text.contains(marker))) - }; - - if contains(&BRAZIL_MARKERS) { - Language::PortugueseBrazil - } else if contains(&PORTUGAL_MARKERS) { - Language::PortuguesePortugal - } else { - Language::PortugueseUnverified - } + let code = ISO_639_2_TO_1 + .iter() + .find(|(long, _)| *long == primary) + .map_or(primary, |(_, short)| *short); + Language::Other(code.to_owned()) } #[cfg(test)] @@ -163,13 +126,9 @@ mod tests { Language::PortugueseBrazil ); assert_eq!( - resolve(Some("por"), None, Some("Português Europeu")), + resolve(Some("por"), None, Some("Português (Portugal)")), Language::PortuguesePortugal ); - assert_eq!( - resolve(Some("por"), Some("Dublado"), None), - Language::PortugueseBrazil - ); } #[test]