Files
arr/crates/arr-parse/src/markers.rs
T

186 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Token classification tables. Input tokens are lowercase.
use crate::{Codec, Edition, HdrMarker, LanguageMarker, Resolution, Source};
/// What a recognised token claims. `Audio` and `Junk` are recognised so they
/// anchor the title boundary and group detection, but are not surfaced.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Marker {
Resolution(Resolution),
Source(Source),
Codec(Codec),
Hdr(HdrMarker),
Language(LanguageMarker),
Edition(Edition),
Audio,
Junk,
}
/// Strong markers set the title boundary. Weak markers are words that also
/// occur in real titles ("Web", "Cam", "Dual") and only count once the
/// boundary is already known.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Strength {
Strong,
Weak,
}
pub(crate) fn classify(token: &str) -> Option<(Marker, Strength)> {
if let Some(m) = classify_exact(token) {
return Some(m);
}
if is_audio_family(token) {
return Some((Marker::Audio, Strength::Strong));
}
None
}
/// Two-token markers: `WEB DL`, `Dolby Vision`, `Dual Áudio`, `Directors Cut`.
pub(crate) fn classify_pair(a: &str, b: &str) -> Option<(Marker, Strength)> {
let m = match (a, b) {
("dolby", "vision") => (Marker::Hdr(HdrMarker::DolbyVision), Strength::Strong),
("web", "dl") => (Marker::Source(Source::WebDl), Strength::Strong),
("web", "rip") => (Marker::Source(Source::WebRip), Strength::Strong),
("blu", "ray") => (Marker::Source(Source::BluRay), Strength::Strong),
("dual", "audio" | "áudio") => (Marker::Language(LanguageMarker::Dual), Strength::Strong),
("multi", "audio") => (Marker::Language(LanguageMarker::Multi), Strength::Strong),
("h" | "x", "264") => (Marker::Codec(Codec::X264), Strength::Strong),
("h" | "x", "265") => (Marker::Codec(Codec::X265), Strength::Strong),
("directors" | "director's", "cut") => {
(Marker::Edition(Edition::DirectorsCut), Strength::Weak)
}
("extended", "cut" | "edition") => (Marker::Edition(Edition::Extended), Strength::Weak),
("special", "edition") => (Marker::Edition(Edition::SpecialEdition), Strength::Weak),
("imax", "enhanced") => (Marker::Edition(Edition::Imax), Strength::Weak),
("season", n) if !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()) => {
(Marker::Junk, Strength::Strong)
}
("seasons", n) if is_season_count(n) => (Marker::Junk, Strength::Strong),
_ => return None,
};
Some(m)
}
/// `1`, `1-3`: the count after a spelled-out `Seasons`. Each part is 1-2
/// digits so a trailing year (`Four Seasons 2024`) never reads as a count.
fn is_season_count(n: &str) -> bool {
n.split('-')
.all(|part| (1..=2).contains(&part.len()) && part.bytes().all(|b| b.is_ascii_digit()))
}
#[allow(clippy::too_many_lines)]
fn classify_exact(t: &str) -> Option<(Marker, Strength)> {
use Strength::{Strong, Weak};
let m = match t {
"2160p" | "4k" | "uhd" => (Marker::Resolution(Resolution::P2160), Strong),
"1080p" | "1080i" => (Marker::Resolution(Resolution::P1080), Strong),
"720p" => (Marker::Resolution(Resolution::P720), Strong),
"576p" | "576i" => (Marker::Resolution(Resolution::P576), Strong),
"480p" | "480i" => (Marker::Resolution(Resolution::P480), Strong),
"remux" | "bdremux" => (Marker::Source(Source::Remux), Strong),
"bluray" | "blu-ray" | "bdrip" | "brrip" => (Marker::Source(Source::BluRay), Strong),
"web-dl" | "webdl" => (Marker::Source(Source::WebDl), Strong),
"webrip" | "web-rip" => (Marker::Source(Source::WebRip), Strong),
"web" => (Marker::Source(Source::WebDl), Weak),
"hdtv" | "pdtv" => (Marker::Source(Source::Hdtv), Strong),
"dvdrip" | "dvd" => (Marker::Source(Source::Dvd), Strong),
"telesync" | "hdts" => (Marker::Source(Source::Telesync), Strong),
"ts" => (Marker::Source(Source::Telesync), Weak),
"telecine" | "hdtc" => (Marker::Source(Source::Telecine), Strong),
"tc" => (Marker::Source(Source::Telecine), Weak),
"screener" | "dvdscr" | "bdscr" => (Marker::Source(Source::Screener), Strong),
"scr" => (Marker::Source(Source::Screener), Weak),
"camrip" | "hdcam" => (Marker::Source(Source::Cam), Strong),
"cam" => (Marker::Source(Source::Cam), Weak),
"x264" | "h264" | "avc" => (Marker::Codec(Codec::X264), Strong),
"x265" | "h265" | "hevc" => (Marker::Codec(Codec::X265), Strong),
"av1" => (Marker::Codec(Codec::Av1), Strong),
"xvid" | "divx" => (Marker::Codec(Codec::Xvid), Strong),
"dovi" => (Marker::Hdr(HdrMarker::DolbyVision), Strong),
"dv" => (Marker::Hdr(HdrMarker::DolbyVision), Weak),
"hdr10+" | "hdr10plus" => (Marker::Hdr(HdrMarker::Hdr10Plus), Strong),
"hdr10" => (Marker::Hdr(HdrMarker::Hdr10), Strong),
"hdr" => (Marker::Hdr(HdrMarker::Hdr), Strong),
"hlg" => (Marker::Hdr(HdrMarker::Hlg), Strong),
"sdr" => (Marker::Hdr(HdrMarker::Sdr), Weak),
// pt-BR self-identification (`DESIGN.md` §5.2): loud and distinctive.
"pt-br" | "ptbr" | "dublado" | "nacional" => {
(Marker::Language(LanguageMarker::PtBr), Strong)
}
"pt-pt" | "ptpt" => (Marker::Language(LanguageMarker::PtPt), Strong),
"portuguese" | "português" | "portugues" => {
(Marker::Language(LanguageMarker::Portuguese), Strong)
}
"por" | "pt" => (Marker::Language(LanguageMarker::Portuguese), Weak),
"multi" => (Marker::Language(LanguageMarker::Multi), Strong),
"dual" => (Marker::Language(LanguageMarker::Dual), Weak),
"english" => (Marker::Language(LanguageMarker::English), Strong),
"eng" => (Marker::Language(LanguageMarker::English), Weak),
"french" | "truefrench" | "vostfr" => (Marker::Language(LanguageMarker::French), Strong),
"fre" | "fra" | "vf" | "vff" => (Marker::Language(LanguageMarker::French), Weak),
"german" | "deutsch" => (Marker::Language(LanguageMarker::German), Strong),
"ger" => (Marker::Language(LanguageMarker::German), Weak),
"spanish" | "castellano" | "latino" => (Marker::Language(LanguageMarker::Spanish), Strong),
"spa" | "esp" => (Marker::Language(LanguageMarker::Spanish), Weak),
"italian" | "italiano" => (Marker::Language(LanguageMarker::Italian), Strong),
"ita" => (Marker::Language(LanguageMarker::Italian), Weak),
"extended" => (Marker::Edition(Edition::Extended), Weak),
"unrated" => (Marker::Edition(Edition::Unrated), Weak),
"uncut" => (Marker::Edition(Edition::Uncut), Weak),
"theatrical" => (Marker::Edition(Edition::Theatrical), Weak),
"imax" => (Marker::Edition(Edition::Imax), Weak),
"remastered" => (Marker::Edition(Edition::Remastered), Weak),
"criterion" => (Marker::Edition(Edition::Criterion), Weak),
"limited" => (Marker::Edition(Edition::Limited), Weak),
"truehd" | "atmos" | "flac" | "opus" | "mp3" | "dd+" | "ma" => (Marker::Audio, Strong),
// "Legendado" claims pt-BR *subtitles* over original audio — it must
// NOT become a PtBr audio claim or main-root originals get rejected.
"proper" | "repack" | "internal" | "hybrid" | "10bit" | "8bit" | "hi10p" | "amzn"
| "dsnp" | "atvp" | "hmax" | "pcok" | "legendado" | "complete" => (Marker::Junk, Strong),
"nf" | "hulu" | "max" | "itunes" | "cr" | "sample" | "subbed" | "subs" | "sub" | "mkv"
| "mp4" | "avi" | "www" | "com" | "net" | "org" | "retail" | "readnfo" => {
(Marker::Junk, Weak)
}
_ => return None,
};
Some(m)
}
fn is_audio_family(t: &str) -> bool {
const PREFIXES: [&str; 9] = [
"ddp", "dd5", "dd7", "dd2", "dts", "eac3", "ac3", "aac", "truehd",
];
PREFIXES.iter().any(|p| {
t.strip_prefix(p).is_some_and(|rest| {
matches!(rest, "-hd" | "hd" | "-x" | "x" | "-ma" | "ma" | "-hd-ma")
|| rest
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '.' | '+' | '-'))
})
})
}
/// `S01`, `S01E02`, `S01E01E02`, `S01-E02`, `1x02`, `S01-S03`, an air
/// date. Anything that ends the title region on a TV tag.
pub(crate) fn is_season_episode(t: &str) -> bool {
crate::episodes::tag_of(t).is_some()
}
/// A plausible release year: four digits, 1900–2099.
pub(crate) fn year_of(t: &str) -> Option<u16> {
if t.len() == 4 && t.bytes().all(|b| b.is_ascii_digit()) {
let y: u16 = t.parse().ok()?;
if (1900..=2099).contains(&y) {
return Some(y);
}
}
None
}