186 lines
8.6 KiB
Rust
186 lines
8.6 KiB
Rust
//! 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
|
||
}
|