Files
arr/crates/arr-parse/src/markers.rs
naps62-yolo 8d44225b57
ci / web (push) Successful in 50s
ci / rust (push) Successful in 1m18s
e2e / e2e (push) Successful in 1m46s
feat(indexer): tvsearch with season and episode tags (#78)
2026-08-22 22:34:04 +01:00

178 lines
8.2 KiB
Rust
Raw Permalink 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`, `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
}