Files
arr/crates/arr-parse/src/markers.rs
T
naps62-yolo 3e57cf5bf8
ci / web (push) Successful in 19s
ci / rust (push) Successful in 56s
e2e / e2e (push) Successful in 1m21s
Release name parser (#50)
2026-08-22 19:49:05 +01:00

205 lines
9.0 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)
}
_ => return None,
};
Some(m)
}
#[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" => (Marker::Junk, Strong),
"nf" | "hulu" | "max" | "itunes" | "cr" | "complete" | "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`.
pub(crate) fn is_season_episode(t: &str) -> bool {
fn digits(s: &str) -> (usize, &str) {
let n = s.bytes().take_while(u8::is_ascii_digit).count();
(n, &s[n..])
}
if let Some(rest) = t.strip_prefix('s') {
let (n, mut rest) = digits(rest);
if !(1..=2).contains(&n) {
return false;
}
while !rest.is_empty() {
let Some(r) = rest.strip_prefix('e').or_else(|| rest.strip_prefix("-e")) else {
return false;
};
let (m, r2) = digits(r);
if !(1..=3).contains(&m) {
return false;
}
rest = r2;
}
return true;
}
let (n, rest) = digits(t);
if (1..=2).contains(&n) {
if let Some(r) = rest.strip_prefix('x') {
let (m, r2) = digits(r);
return (2..=3).contains(&m) && r2.is_empty();
}
}
false
}
/// 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
}