refactor(probe): use arr-core for pt-PT versus pt-BR
ci / rust (pull_request) Successful in 1m22s
ci / web (pull_request) Successful in 8s
e2e / e2e (pull_request) Successful in 59s

#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 <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-08-22 20:45:54 +01:00
co-authored by Claude Opus 5
parent 14fa9e43d8
commit 60f7ddda2a
+30 -71
View File
@@ -1,28 +1,15 @@
//! Turning a track's language tag into an [`arr_core::Language`]. //! 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 //! The Portuguese case is not decided here. `arr-core::lang` owns the §5.2
//! variants, so `ffprobe` reports `por` for a Lisbon dub and a São Paulo one //! signal order, and this module's job is to hand it the two signals a file
//! alike. DESIGN.md §5.2 lists three signals; two of them are visible here — //! carries — the stream `title` and `handler_name` strings, and the
//! the stream `title` and `handler_name` strings, and container-level BCP-47 //! container's BCP-47 tag. The third signal, release-name markers, belongs to
//! tags. The third, release-name markers, belongs to `arr-parse` and is //! the release and is deliberately not consulted from a file probe.
//! 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.
use arr_core::Language; use arr_core::{
lang::{resolve_portuguese, PortugueseEvidence},
/// Strings a Brazilian track labels itself with. Language,
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",
];
/// ISO-639-2 to ISO-639-1, for the codes `ffprobe` actually emits. /// 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"), ("bul", "bg"),
("cat", "ca"), ("cat", "ca"),
("ces", "cs"), ("ces", "cs"),
("chi", "zh"),
("cze", "cs"), ("cze", "cs"),
("dan", "da"), ("dan", "da"),
("deu", "de"), ("deu", "de"),
@@ -66,59 +54,34 @@ const ISO_639_2_TO_1: [(&str, &str); 40] = [
("tha", "th"), ("tha", "th"),
("tur", "tr"), ("tur", "tr"),
("ukr", "uk"), ("ukr", "uk"),
("und", "und"),
("vie", "vi"), ("vie", "vi"),
("zho", "zh"), ("zho", "zh"),
("chi", "zh"),
("und", "und"),
]; ];
/// Resolve one track's language from its tag and its self-description. /// 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 { 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 normalised = tag.unwrap_or("und").trim().to_ascii_lowercase();
let (primary, region) = match tag.split_once(['-', '_']) { let primary = normalised
Some((primary, rest)) => (primary, rest.split(['-', '_']).next().unwrap_or("")), .split(['-', '_'])
None => (tag.as_str(), ""), .next()
}; .unwrap_or(normalised.as_str());
if !matches!(primary, "pt" | "por") { if matches!(primary, "pt" | "por") {
let code = ISO_639_2_TO_1 return resolve_portuguese(PortugueseEvidence {
.iter() // Signal 1 is a property of the release, not of the file.
.find(|(long, _)| *long == primary) name_markers: &[],
.map_or(primary, |(_, short)| *short); stream_title: title,
return Language::Other(code.to_owned()); handler_name: handler,
container_tag: tag,
});
} }
match region { let code = ISO_639_2_TO_1
"br" => return Language::PortugueseBrazil, .iter()
"pt" => return Language::PortuguesePortugal, .find(|(long, _)| *long == primary)
_ => {} .map_or(primary, |(_, short)| *short);
} Language::Other(code.to_owned())
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::<Vec<_>>();
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
}
} }
#[cfg(test)] #[cfg(test)]
@@ -163,13 +126,9 @@ mod tests {
Language::PortugueseBrazil Language::PortugueseBrazil
); );
assert_eq!( assert_eq!(
resolve(Some("por"), None, Some("Português Europeu")), resolve(Some("por"), None, Some("Português (Portugal)")),
Language::PortuguesePortugal Language::PortuguesePortugal
); );
assert_eq!(
resolve(Some("por"), Some("Dublado"), None),
Language::PortugueseBrazil
);
} }
#[test] #[test]