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`].
//!
//! 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::<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
}
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]