252 lines
8.9 KiB
Rust
252 lines
8.9 KiB
Rust
//! pt-PT versus pt-BR detection (`DESIGN.md` §5.2).
|
|
//!
|
|
//! ISO-639-2 has one code, `por`, for both variants, so the variant must be
|
|
//! recovered from side channels. Three signals, tried in order:
|
|
//!
|
|
//! 1. release-name markers (`PT-BR`, `Dublado`, `Nacional` versus `PT-PT`),
|
|
//! 2. `ffprobe` stream *title* and `handler_name` strings,
|
|
//! 3. container-level BCP-47 tags (Matroska can carry `pt-BR`).
|
|
//!
|
|
//! When none resolve it — including when a signal contradicts itself — the
|
|
//! track is tagged [`Language::PortugueseUnverified`] and surfaced. Never
|
|
//! guess: guessing toward `kids` gives a child a Brazilian dub, guessing
|
|
//! toward `main` rejects a Brazilian film's own audio.
|
|
|
|
use arr_parse::LanguageMarker;
|
|
|
|
use crate::Language;
|
|
|
|
/// Everything available to disambiguate a Portuguese audio track.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct PortugueseEvidence<'a> {
|
|
/// Language markers parsed from the release name (signal 1).
|
|
pub name_markers: &'a [LanguageMarker],
|
|
/// `ffprobe` stream title (signal 2), often literally
|
|
/// `Portuguese (Brazil)`.
|
|
pub stream_title: Option<&'a str>,
|
|
/// `ffprobe` stream `handler_name` (signal 2).
|
|
pub handler_name: Option<&'a str>,
|
|
/// Container-level BCP-47 tag (signal 3), e.g. `pt-BR`.
|
|
pub container_tag: Option<&'a str>,
|
|
}
|
|
|
|
/// Which variant one signal claims, if any.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum Variant {
|
|
Portugal,
|
|
Brazil,
|
|
}
|
|
|
|
impl Variant {
|
|
const fn language(self) -> Language {
|
|
match self {
|
|
Self::Portugal => Language::PortuguesePortugal,
|
|
Self::Brazil => Language::PortugueseBrazil,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve a `por` audio track to a concrete Portuguese variant, or
|
|
/// [`Language::PortugueseUnverified`] when no signal settles it.
|
|
#[must_use]
|
|
pub fn resolve_portuguese(evidence: PortugueseEvidence<'_>) -> Language {
|
|
let signals = [
|
|
from_name_markers(evidence.name_markers),
|
|
from_stream_strings(evidence.stream_title, evidence.handler_name),
|
|
evidence.container_tag.and_then(from_bcp47),
|
|
];
|
|
signals
|
|
.into_iter()
|
|
.flatten()
|
|
.next()
|
|
.map_or(Language::PortugueseUnverified, Variant::language)
|
|
}
|
|
|
|
/// Signal 1: explicit variant markers in the release name. A name claiming
|
|
/// both variants (a dual-dub release) resolves nothing for any one track.
|
|
fn from_name_markers(markers: &[LanguageMarker]) -> Option<Variant> {
|
|
let br = markers.contains(&LanguageMarker::PtBr);
|
|
let pt = markers.contains(&LanguageMarker::PtPt);
|
|
match (br, pt) {
|
|
(true, false) => Some(Variant::Brazil),
|
|
(false, true) => Some(Variant::Portugal),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Signal 2: free-text stream metadata. Contradictory mentions across the
|
|
/// title and handler resolve nothing.
|
|
fn from_stream_strings(title: Option<&str>, handler: Option<&str>) -> Option<Variant> {
|
|
let mut seen_br = false;
|
|
let mut seen_pt = false;
|
|
for text in [title, handler].into_iter().flatten() {
|
|
let low = text.to_lowercase();
|
|
seen_br |= ["brazil", "brasil", "pt-br", "pt_br", "ptbr"]
|
|
.iter()
|
|
.any(|needle| low.contains(needle));
|
|
seen_pt |= ["portugal", "pt-pt", "pt_pt", "ptpt", "european"]
|
|
.iter()
|
|
.any(|needle| low.contains(needle));
|
|
}
|
|
match (seen_br, seen_pt) {
|
|
(true, false) => Some(Variant::Brazil),
|
|
(false, true) => Some(Variant::Portugal),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Signal 3: a BCP-47 tag whose primary subtag is Portuguese and whose
|
|
/// region subtag names the variant. A bare `pt` resolves nothing.
|
|
fn from_bcp47(tag: &str) -> Option<Variant> {
|
|
let mut subtags = tag.split(['-', '_']).map(str::to_lowercase);
|
|
let primary = subtags.next()?;
|
|
if primary != "pt" && primary != "por" {
|
|
return None;
|
|
}
|
|
for subtag in subtags {
|
|
match subtag.as_str() {
|
|
"br" => return Some(Variant::Brazil),
|
|
"pt" => return Some(Variant::Portugal),
|
|
_ => {}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// The [`Language`] a release-name marker claims, if it claims one at all.
|
|
/// `MULTi` and `DUAL` advertise track counts, not languages. A bare
|
|
/// `Portuguese` marker has unknown flavour: pre-grab there is no stream or
|
|
/// container to consult, so it stays unverified.
|
|
#[must_use]
|
|
pub fn language_of_marker(marker: LanguageMarker) -> Option<Language> {
|
|
match marker {
|
|
LanguageMarker::PtBr => Some(Language::PortugueseBrazil),
|
|
LanguageMarker::PtPt => Some(Language::PortuguesePortugal),
|
|
LanguageMarker::Portuguese => Some(Language::PortugueseUnverified),
|
|
LanguageMarker::English => Some(Language::Other("en".to_owned())),
|
|
LanguageMarker::French => Some(Language::Other("fr".to_owned())),
|
|
LanguageMarker::German => Some(Language::Other("de".to_owned())),
|
|
LanguageMarker::Spanish => Some(Language::Other("es".to_owned())),
|
|
LanguageMarker::Italian => Some(Language::Other("it".to_owned())),
|
|
LanguageMarker::Multi | LanguageMarker::Dual => None,
|
|
}
|
|
}
|
|
|
|
/// Whether a language is a resolved Portuguese variant.
|
|
#[must_use]
|
|
pub const fn is_resolved_portuguese(language: &Language) -> bool {
|
|
matches!(
|
|
language,
|
|
Language::PortuguesePortugal | Language::PortugueseBrazil
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn name_marker_resolves_first() {
|
|
let markers = [LanguageMarker::PtBr];
|
|
let evidence = PortugueseEvidence {
|
|
name_markers: &markers,
|
|
// A contradicting later signal must not win over signal 1.
|
|
stream_title: Some("Portuguese (Portugal)"),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortugueseBrazil);
|
|
|
|
let markers = [LanguageMarker::PtPt];
|
|
let evidence = PortugueseEvidence {
|
|
name_markers: &markers,
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortuguesePortugal);
|
|
}
|
|
|
|
#[test]
|
|
fn contradictory_name_markers_fall_through() {
|
|
let markers = [LanguageMarker::PtBr, LanguageMarker::PtPt];
|
|
let evidence = PortugueseEvidence {
|
|
name_markers: &markers,
|
|
stream_title: Some("Portuguese (Brazil)"),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortugueseBrazil);
|
|
}
|
|
|
|
#[test]
|
|
fn stream_title_resolves_second() {
|
|
let evidence = PortugueseEvidence {
|
|
stream_title: Some("Portuguese (Brazil)"),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortugueseBrazil);
|
|
|
|
let evidence = PortugueseEvidence {
|
|
handler_name: Some("Português (Portugal)"),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortuguesePortugal);
|
|
}
|
|
|
|
#[test]
|
|
fn contradictory_stream_strings_fall_through() {
|
|
let evidence = PortugueseEvidence {
|
|
stream_title: Some("Portuguese (Brazil)"),
|
|
handler_name: Some("Portuguese (Portugal)"),
|
|
container_tag: Some("pt-PT"),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortuguesePortugal);
|
|
}
|
|
|
|
#[test]
|
|
fn bcp47_tag_resolves_third() {
|
|
for (tag, expected) in [
|
|
("pt-BR", Language::PortugueseBrazil),
|
|
("pt-PT", Language::PortuguesePortugal),
|
|
("por-br", Language::PortugueseBrazil),
|
|
("pt_BR", Language::PortugueseBrazil),
|
|
] {
|
|
let evidence = PortugueseEvidence {
|
|
container_tag: Some(tag),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bare_or_foreign_tags_resolve_nothing() {
|
|
for tag in ["pt", "por", "en-US", "pt-AO"] {
|
|
let evidence = PortugueseEvidence {
|
|
container_tag: Some(tag),
|
|
..PortugueseEvidence::default()
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortugueseUnverified);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn no_signal_emits_por_unverified_not_a_guess() {
|
|
let evidence = PortugueseEvidence {
|
|
name_markers: &[LanguageMarker::Multi],
|
|
stream_title: Some("Portuguese"),
|
|
handler_name: Some("SoundHandler"),
|
|
container_tag: Some("por"),
|
|
};
|
|
assert_eq!(resolve_portuguese(evidence), Language::PortugueseUnverified);
|
|
}
|
|
|
|
#[test]
|
|
fn marker_languages_map_without_guessing_portuguese_flavour() {
|
|
assert_eq!(
|
|
language_of_marker(LanguageMarker::Portuguese),
|
|
Some(Language::PortugueseUnverified)
|
|
);
|
|
assert_eq!(language_of_marker(LanguageMarker::Multi), None);
|
|
assert_eq!(language_of_marker(LanguageMarker::Dual), None);
|
|
}
|
|
}
|