use once_cell::sync::Lazy;
use regex::Regex;
pub mod regexes {
use super::{Lazy, Regex};
pub static SOURCE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?ix)
\b(?:
(?P<bluray>BluRay|Blu-Ray|HD-?DVD|BDMux|BD)|
(?P<webdl>
WEB[-_.\x20]DL(?:mux)?|WEBDL|AmazonHD|AmazonSD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|HBOMaxHD|DisneyHD|
[.\x20]WEB[.\x20](?:[xh][ .]?26[45]|AVC|HEVC|DDP?5[. ]1)|
[.\x20](?-i:WEB)$|
(?:720|1080|2160)p[-.\x20]WEB[-.\x20]|
[-.\x20]WEB[-.\x20](?:720|1080|2160)p|
\b\s/\sWEB\s/\s\b|
(?P<provider_web>(?:AMZN|NF|DP)[.\x20-]WEB[.\x20-])
)|
(?P<webrip>WebRip|Web-Rip|WEBMux)|
(?P<hdtv>HDTV)|
(?P<bdrip>BDRip|BDLight)|
(?P<brrip>BRRip)|
(?P<dvd>DVD|DVDRip|NTSC|PAL|xvidvd)|
(?P<dsr>WS[-_.\x20]DSR|DSR)|
(?P<pdtv>PDTV)|
(?P<sdtv>SDTV)|
(?P<tvrip>TVRip)
)(?:\b|$|[\x20.])",
)
.expect("SOURCE_REGEX must compile")
});
pub static RAW_HD_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(?P<rawhd>RawHD|Raw[-_. ]HD)\b").expect("RAW_HD_REGEX must compile")
});
pub static MPEG2_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\b(?P<mpeg2>MPEG[-_. ]?2)\b").expect("MPEG2_REGEX must compile"));
pub static REMUX_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)(?:[_. ]|\d{4}p-|\bHybrid-)(?P<remux>(?:(BD|UHD)[-_. ]?)?Remux)\b|(?P<remux_post>(?:(BD|UHD)[-_. ]?)?Remux[_. ]\d{4}p)",
)
.expect("REMUX_REGEX must compile")
});
pub static ANIME_BLURAY_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)bd(?:720|1080|2160)|bd").expect("ANIME_BLURAY_REGEX must compile")
});
pub static ANIME_WEBDL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\[WEB\]|[\[(]WEB[ .]").expect("ANIME_WEBDL_REGEX must compile")
});
pub static RESOLUTION_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)\b(?:(?P<R360p>360p)|(?P<R480p>480p|480i|640x480|848x480)|(?P<R540p>540p)|(?P<R576p>576p)|(?P<R720p>720p|1280x720|960p)|(?P<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|(?P<R2160p>2160p|3840x2160|4k[-_. ](?:UHD|HEVC|BD|H265)|(?:UHD|HEVC|BD|H265)[-_. ]4k))\b",
)
.expect("RESOLUTION_REGEX must compile")
});
pub static ALTERNATIVE_RESOLUTION_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(?P<R2160p>UHD)\b|(?P<R2160p_alt>\[4K\])")
.expect("ALTERNATIVE_RESOLUTION_REGEX must compile")
});
pub static CODEC_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)\b(?:(?P<x264>x264)|(?P<h264>h264)|(?P<xvidhd>XvidHD)|(?P<xvid>Xvid)|(?P<divx>divx))\b",
)
.expect("CODEC_REGEX must compile")
});
pub static OTHER_SOURCE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(?P<hdtv>HD[-_. ]TV)|(?P<sdtv>SD[-_. ]TV)")
.expect("OTHER_SOURCE_REGEX must compile")
});
pub static HIGH_DEF_PDTV_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)hr[-_. ]ws").expect("HIGH_DEF_PDTV_REGEX must compile"));
pub static PROPER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)\b(?P<proper>proper)\b").expect("PROPER_REGEX must compile"));
pub static REPACK_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(?P<repack>repack\d?|rerip\d?)\b").expect("REPACK_REGEX must compile")
});
pub static VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)\d[-._ ]?v(?P<version>\d)[-._ ]|\[v(?P<version2>\d)\]|repack(?P<version3>\d)|rerip(?P<version4>\d)|(?:480|576|720|1080|2160)p[._ ]v(?P<version5>\d)",
)
.expect("VERSION_REGEX must compile")
});
pub static REAL_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\b(?P<real>REAL)\b").expect("REAL_REGEX must compile"));
}
use regexes::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceMatch<'a> {
pub group: SourceGroup,
pub matched: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SourceGroup {
Bluray,
Webdl,
Webrip,
Hdtv,
Bdrip,
Brrip,
Dvd,
Dsr,
Pdtv,
Sdtv,
Tvrip,
}
fn classify_source_capture<'a>(s: &'a str, caps: ®ex::Captures<'a>) -> Option<SourceMatch<'a>> {
let (group, mat) = if let Some(m) = caps.name("bluray") {
(SourceGroup::Bluray, m)
} else if let Some(m) = caps.name("webdl") {
(SourceGroup::Webdl, m)
} else if let Some(m) = caps.name("webrip") {
(SourceGroup::Webrip, m)
} else if let Some(m) = caps.name("hdtv") {
(SourceGroup::Hdtv, m)
} else if let Some(m) = caps.name("bdrip") {
(SourceGroup::Bdrip, m)
} else if let Some(m) = caps.name("brrip") {
(SourceGroup::Brrip, m)
} else if let Some(m) = caps.name("dvd") {
(SourceGroup::Dvd, m)
} else if let Some(m) = caps.name("dsr") {
(SourceGroup::Dsr, m)
} else if let Some(m) = caps.name("pdtv") {
(SourceGroup::Pdtv, m)
} else if let Some(m) = caps.name("sdtv") {
(SourceGroup::Sdtv, m)
} else if let Some(m) = caps.name("tvrip") {
(SourceGroup::Tvrip, m)
} else {
return None;
};
if group == SourceGroup::Bluray
&& mat.as_str().eq_ignore_ascii_case("BD")
&& mat.end() == s.len()
{
return None;
}
if group == SourceGroup::Webdl && caps.name("provider_web").is_some() {
let tail = &s[mat.end()..];
if tail.len() >= 3 && tail.as_bytes()[..3].eq_ignore_ascii_case(b"Rip") {
return Some(SourceMatch {
group: SourceGroup::Webrip,
matched: mat.as_str(),
});
}
}
Some(SourceMatch {
group,
matched: mat.as_str(),
})
}
#[cfg(test)]
pub(crate) fn match_source(s: &str) -> Option<SourceMatch<'_>> {
for caps in SOURCE_REGEX.captures_iter(s) {
if let Some(m) = classify_source_capture(s, &caps) {
return Some(m);
}
}
None
}
pub(crate) fn match_source_last(s: &str) -> Option<SourceMatch<'_>> {
let mut last: Option<SourceMatch<'_>> = None;
for caps in SOURCE_REGEX.captures_iter(s) {
if let Some(m) = classify_source_capture(s, &caps) {
last = Some(m);
}
}
last
}
pub(crate) fn matches_anime_bluray(s: &str) -> bool {
for m in ANIME_BLURAY_REGEX.find_iter(s) {
let matched = m.as_str();
if matched.len() == 2 {
let bytes = s.as_bytes();
let start = m.start();
let end = m.end();
let before_ok = start > 0
&& matches!(
bytes.get(start - 1),
Some(b'-' | b'_' | b'.' | b' ' | b'(' | b'[')
);
let after_ok = end < s.len()
&& matches!(
bytes.get(end),
Some(b'-' | b'_' | b'.' | b' ' | b')' | b']')
);
if before_ok && after_ok {
return true;
}
} else {
return true;
}
}
false
}
pub(crate) fn match_remux(s: &str) -> Option<&str> {
for caps in REMUX_REGEX.captures_iter(s) {
if let Some(m) = caps.name("remux").or_else(|| caps.name("remux_post")) {
return Some(m.as_str());
}
}
None
}
pub(crate) fn matches_alternative_resolution(s: &str) -> bool {
ALTERNATIVE_RESOLUTION_REGEX
.captures_iter(s)
.any(|caps| caps.name("R2160p").is_some() || caps.name("R2160p_alt").is_some())
}
fn detect_resolution(name: &str) -> crate::quality::Resolution {
use crate::quality::Resolution;
if let Some(caps) = RESOLUTION_REGEX.captures(name) {
if caps.name("R360p").is_some() {
return Resolution::R360p;
}
if caps.name("R480p").is_some() {
return Resolution::R480p;
}
if caps.name("R540p").is_some() {
return Resolution::R540p;
}
if caps.name("R576p").is_some() {
return Resolution::R576p;
}
if caps.name("R720p").is_some() {
return Resolution::R720p;
}
if caps.name("R1080p").is_some() {
return Resolution::R1080p;
}
if caps.name("R2160p").is_some() {
return Resolution::R2160p;
}
}
if matches_alternative_resolution(name) {
return Resolution::R2160p;
}
Resolution::Unknown
}
fn quality_source(q: crate::quality::Quality) -> crate::quality::QualitySource {
use crate::quality::{Quality, QualitySource};
match q {
Quality::Unknown => QualitySource::Unknown,
Quality::Sdtv => QualitySource::Television,
Quality::Hdtv720p => QualitySource::Television,
Quality::Hdtv1080p => QualitySource::Television,
Quality::Hdtv2160p => QualitySource::Television,
Quality::RawHd => QualitySource::TelevisionRaw,
Quality::Webdl480p => QualitySource::Web,
Quality::Webdl720p => QualitySource::Web,
Quality::Webdl1080p => QualitySource::Web,
Quality::Webdl2160p => QualitySource::Web,
Quality::Webrip480p => QualitySource::WebRip,
Quality::Webrip720p => QualitySource::WebRip,
Quality::Webrip1080p => QualitySource::WebRip,
Quality::Webrip2160p => QualitySource::WebRip,
Quality::Dvd => QualitySource::Dvd,
Quality::Bluray480p => QualitySource::Bluray,
Quality::Bluray576p => QualitySource::Bluray,
Quality::Bluray720p => QualitySource::Bluray,
Quality::Bluray1080p => QualitySource::Bluray,
Quality::Bluray2160p => QualitySource::Bluray,
Quality::Bluray1080pRemux => QualitySource::BlurayRaw,
Quality::Bluray2160pRemux => QualitySource::BlurayRaw,
}
}
fn quality_for_extension(extension: &str) -> crate::quality::Quality {
use crate::quality::Quality;
const TABLE: &[(&str, Quality)] = &[
(".webm", Quality::Unknown),
(".m4v", Quality::Sdtv),
(".3gp", Quality::Sdtv),
(".nsv", Quality::Sdtv),
(".ty", Quality::Sdtv),
(".strm", Quality::Sdtv),
(".rm", Quality::Sdtv),
(".rmvb", Quality::Sdtv),
(".m3u", Quality::Sdtv),
(".ifo", Quality::Sdtv),
(".mov", Quality::Sdtv),
(".qt", Quality::Sdtv),
(".divx", Quality::Sdtv),
(".xvid", Quality::Sdtv),
(".bivx", Quality::Sdtv),
(".nrg", Quality::Sdtv),
(".pva", Quality::Sdtv),
(".wmv", Quality::Sdtv),
(".asf", Quality::Sdtv),
(".asx", Quality::Sdtv),
(".ogm", Quality::Sdtv),
(".ogv", Quality::Sdtv),
(".m2v", Quality::Sdtv),
(".avi", Quality::Sdtv),
(".bin", Quality::Sdtv),
(".dat", Quality::Sdtv),
(".dvr-ms", Quality::Sdtv),
(".mpg", Quality::Sdtv),
(".mpeg", Quality::Sdtv),
(".mp4", Quality::Sdtv),
(".avc", Quality::Sdtv),
(".vp3", Quality::Sdtv),
(".svq3", Quality::Sdtv),
(".nuv", Quality::Sdtv),
(".viv", Quality::Sdtv),
(".dv", Quality::Sdtv),
(".fli", Quality::Sdtv),
(".flv", Quality::Sdtv),
(".wpl", Quality::Sdtv),
(".img", Quality::Dvd),
(".iso", Quality::Dvd),
(".vob", Quality::Dvd),
(".mkv", Quality::Hdtv720p),
(".ts", Quality::Hdtv720p),
(".wtv", Quality::Hdtv720p),
(".m2ts", Quality::Bluray720p),
];
if extension.is_empty() {
return Quality::Unknown;
}
for (ext, q) in TABLE {
if extension.eq_ignore_ascii_case(ext) {
return *q;
}
}
Quality::Unknown
}
fn get_path_extension(path: &str) -> &str {
if let Some(idx) = path.rfind('.')
&& idx + 1 < path.len()
{
return &path[idx..];
}
""
}
fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
let n = needle.len();
if n == 0 {
return true;
}
let h = haystack.as_bytes();
if h.len() < n {
return false;
}
h.windows(n)
.any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
}
pub fn parse_quality_name(name: &str) -> crate::quality::QualityModel {
use crate::quality::{
Quality, QualityDetectionSource, QualityModel, QualitySource, Resolution,
};
let raw = name.trim();
let normalized = raw.replace('_', " ");
let normalized = normalized.trim();
let (revision, revision_detection_source) = parse_quality_modifiers(raw, normalized);
if RAW_HD_REGEX.is_match(normalized) {
return QualityModel {
quality: Quality::RawHd,
revision,
source_detection_source: QualityDetectionSource::Name,
resolution_detection_source: QualityDetectionSource::Name,
revision_detection_source,
};
}
let source_match = match_source_last(normalized);
let resolution = detect_resolution(normalized);
let codec_caps = CODEC_REGEX.captures(normalized);
let codec_xvid = codec_caps.as_ref().and_then(|c| c.name("xvid")).is_some();
let codec_divx = codec_caps.as_ref().and_then(|c| c.name("divx")).is_some();
let remux_match = match_remux(normalized).is_some();
let resolution_detection_source = if resolution != Resolution::Unknown {
QualityDetectionSource::Name
} else {
QualityDetectionSource::Unknown
};
let mk = |quality: Quality, resolution_source: QualityDetectionSource| QualityModel {
quality,
revision,
source_detection_source: QualityDetectionSource::Name,
resolution_detection_source: resolution_source,
revision_detection_source,
};
if let Some(sm) = source_match {
match sm.group {
SourceGroup::Bluray => {
if codec_xvid || codec_divx {
return mk(Quality::Bluray480p, resolution_detection_source);
}
match resolution {
Resolution::R2160p => {
let q = if remux_match {
Quality::Bluray2160pRemux
} else {
Quality::Bluray2160p
};
return mk(q, resolution_detection_source);
}
Resolution::R1080p => {
let q = if remux_match {
Quality::Bluray1080pRemux
} else {
Quality::Bluray1080p
};
return mk(q, resolution_detection_source);
}
Resolution::R576p => {
return mk(Quality::Bluray576p, resolution_detection_source);
}
Resolution::R360p | Resolution::R480p | Resolution::R540p => {
return mk(Quality::Bluray480p, resolution_detection_source);
}
Resolution::R720p => {
return mk(Quality::Bluray720p, resolution_detection_source);
}
Resolution::Unknown => {
if remux_match {
return mk(Quality::Bluray1080pRemux, resolution_detection_source);
}
return mk(Quality::Bluray720p, resolution_detection_source);
}
}
}
SourceGroup::Webdl => match resolution {
Resolution::R2160p => return mk(Quality::Webdl2160p, resolution_detection_source),
Resolution::R1080p => return mk(Quality::Webdl1080p, resolution_detection_source),
Resolution::R720p => return mk(Quality::Webdl720p, resolution_detection_source),
_ => {
if raw.contains("[WEBDL]") {
return mk(Quality::Webdl720p, resolution_detection_source);
}
return mk(Quality::Webdl480p, resolution_detection_source);
}
},
SourceGroup::Webrip => match resolution {
Resolution::R2160p => return mk(Quality::Webrip2160p, resolution_detection_source),
Resolution::R1080p => return mk(Quality::Webrip1080p, resolution_detection_source),
Resolution::R720p => return mk(Quality::Webrip720p, resolution_detection_source),
_ => return mk(Quality::Webrip480p, resolution_detection_source),
},
SourceGroup::Hdtv => {
if MPEG2_REGEX.is_match(normalized) {
return mk(Quality::RawHd, resolution_detection_source);
}
match resolution {
Resolution::R2160p => {
return mk(Quality::Hdtv2160p, resolution_detection_source);
}
Resolution::R1080p => {
return mk(Quality::Hdtv1080p, resolution_detection_source);
}
Resolution::R720p => return mk(Quality::Hdtv720p, resolution_detection_source),
_ => {
if raw.contains("[HDTV]") {
return mk(Quality::Hdtv720p, resolution_detection_source);
}
return mk(Quality::Sdtv, resolution_detection_source);
}
}
}
SourceGroup::Bdrip | SourceGroup::Brrip => match resolution {
Resolution::R720p => return mk(Quality::Bluray720p, resolution_detection_source),
Resolution::R1080p => return mk(Quality::Bluray1080p, resolution_detection_source),
Resolution::R2160p => return mk(Quality::Bluray2160p, resolution_detection_source),
_ => return mk(Quality::Bluray480p, resolution_detection_source),
},
SourceGroup::Dvd => return mk(Quality::Dvd, resolution_detection_source),
SourceGroup::Pdtv | SourceGroup::Sdtv | SourceGroup::Dsr | SourceGroup::Tvrip => {
if resolution == Resolution::R1080p
|| contains_ignore_ascii_case(normalized, "1080p")
{
return mk(Quality::Hdtv1080p, resolution_detection_source);
}
if resolution == Resolution::R720p || contains_ignore_ascii_case(normalized, "720p")
{
return mk(Quality::Hdtv720p, resolution_detection_source);
}
if HIGH_DEF_PDTV_REGEX.is_match(normalized) {
return mk(Quality::Hdtv720p, QualityDetectionSource::Name);
}
return mk(Quality::Sdtv, resolution_detection_source);
}
}
}
let mk_named = |quality: Quality, resolution_source: QualityDetectionSource| QualityModel {
quality,
revision,
source_detection_source: QualityDetectionSource::Name,
resolution_detection_source: resolution_source,
revision_detection_source,
};
if source_match.is_none() && remux_match && resolution != Resolution::Unknown {
let q = match resolution {
Resolution::R480p => Some(Quality::Bluray480p),
Resolution::R720p => Some(Quality::Bluray720p),
Resolution::R2160p => Some(Quality::Bluray2160pRemux),
Resolution::R1080p => Some(Quality::Bluray1080pRemux),
_ => None,
};
if let Some(quality) = q {
return QualityModel {
quality,
revision,
source_detection_source: QualityDetectionSource::Unknown,
resolution_detection_source,
revision_detection_source,
};
}
}
if matches_anime_bluray(normalized) {
let resolution_source_anime = QualityDetectionSource::Name;
if matches!(
resolution,
Resolution::R360p | Resolution::R480p | Resolution::R540p | Resolution::R576p
) || contains_ignore_ascii_case(normalized, "480p")
{
return mk_named(Quality::Dvd, resolution_source_anime);
}
if resolution == Resolution::R1080p || contains_ignore_ascii_case(normalized, "1080p") {
let q = if remux_match {
Quality::Bluray1080pRemux
} else {
Quality::Bluray1080p
};
return mk_named(q, resolution_source_anime);
}
if resolution == Resolution::R2160p || contains_ignore_ascii_case(normalized, "2160p") {
let q = if remux_match {
Quality::Bluray2160pRemux
} else {
Quality::Bluray2160p
};
return mk_named(q, resolution_source_anime);
}
if remux_match && resolution != Resolution::R720p {
return mk_named(Quality::Bluray1080pRemux, resolution_detection_source);
}
return mk_named(Quality::Bluray720p, resolution_detection_source);
}
if ANIME_WEBDL_REGEX.is_match(normalized) {
let resolution_source_anime = QualityDetectionSource::Name;
if matches!(
resolution,
Resolution::R360p | Resolution::R480p | Resolution::R540p | Resolution::R576p
) || contains_ignore_ascii_case(normalized, "480p")
{
return mk_named(Quality::Webdl480p, resolution_source_anime);
}
if resolution == Resolution::R1080p || contains_ignore_ascii_case(normalized, "1080p") {
return mk_named(Quality::Webdl1080p, resolution_source_anime);
}
if resolution == Resolution::R2160p || contains_ignore_ascii_case(normalized, "2160p") {
return mk_named(Quality::Webdl2160p, resolution_source_anime);
}
return mk_named(Quality::Webdl720p, resolution_detection_source);
}
if resolution != Resolution::Unknown {
let mut derived_source = QualitySource::Unknown;
let mut source_detection = QualityDetectionSource::Unknown;
if remux_match {
derived_source = QualitySource::BlurayRaw;
source_detection = QualityDetectionSource::Name;
} else {
let ext = get_path_extension(raw);
let from_ext = quality_for_extension(ext);
if from_ext != Quality::Unknown {
derived_source = quality_source(from_ext);
source_detection = QualityDetectionSource::Extension;
}
}
let mk_resfb = |quality: Quality| QualityModel {
quality,
revision,
source_detection_source: source_detection,
resolution_detection_source: QualityDetectionSource::Name,
revision_detection_source,
};
let q = match resolution {
Resolution::R2160p => {
if derived_source == QualitySource::Unknown {
Quality::Hdtv2160p
} else {
crate::quality::finder::find_by_source_and_resolution(derived_source, 2160)
}
}
Resolution::R1080p => {
if derived_source == QualitySource::Unknown {
Quality::Hdtv1080p
} else {
crate::quality::finder::find_by_source_and_resolution(derived_source, 1080)
}
}
Resolution::R720p => {
if derived_source == QualitySource::Unknown {
Quality::Hdtv720p
} else {
crate::quality::finder::find_by_source_and_resolution(derived_source, 720)
}
}
Resolution::R360p | Resolution::R480p | Resolution::R540p | Resolution::R576p => {
if derived_source == QualitySource::Unknown {
Quality::Sdtv
} else {
crate::quality::finder::find_by_source_and_resolution(derived_source, 480)
}
}
Resolution::Unknown => unreachable!("resolution != Unknown gated by outer if"),
};
return mk_resfb(q);
}
if codec_caps.as_ref().and_then(|c| c.name("x264")).is_some() {
return QualityModel {
quality: Quality::Sdtv,
revision,
source_detection_source: QualityDetectionSource::Unknown,
resolution_detection_source,
revision_detection_source,
};
}
if normalized.contains("848x480") {
if normalized.contains("dvd") {
return mk_named(Quality::Dvd, QualityDetectionSource::Name);
}
if contains_ignore_ascii_case(normalized, "bluray") {
return mk_named(Quality::Bluray480p, QualityDetectionSource::Name);
}
return QualityModel {
quality: Quality::Sdtv,
revision,
source_detection_source: QualityDetectionSource::Unknown,
resolution_detection_source: QualityDetectionSource::Name,
revision_detection_source,
};
}
if contains_ignore_ascii_case(normalized, "1280x720") {
if contains_ignore_ascii_case(normalized, "bluray") {
return mk_named(Quality::Bluray720p, QualityDetectionSource::Name);
}
return QualityModel {
quality: Quality::Hdtv720p,
revision,
source_detection_source: QualityDetectionSource::Unknown,
resolution_detection_source: QualityDetectionSource::Name,
revision_detection_source,
};
}
if contains_ignore_ascii_case(normalized, "1920x1080") {
if contains_ignore_ascii_case(normalized, "bluray") {
return mk_named(Quality::Bluray1080p, QualityDetectionSource::Name);
}
return QualityModel {
quality: Quality::Hdtv1080p,
revision,
source_detection_source: QualityDetectionSource::Unknown,
resolution_detection_source: QualityDetectionSource::Name,
revision_detection_source,
};
}
if contains_ignore_ascii_case(normalized, "bluray720p") {
return mk_named(Quality::Bluray720p, QualityDetectionSource::Name);
}
if contains_ignore_ascii_case(normalized, "bluray1080p") {
return mk_named(Quality::Bluray1080p, QualityDetectionSource::Name);
}
if contains_ignore_ascii_case(normalized, "bluray2160p") {
return mk_named(Quality::Bluray2160p, QualityDetectionSource::Name);
}
if let Some(caps) = OTHER_SOURCE_REGEX.captures(normalized) {
let q = if caps.name("sdtv").is_some() {
Some(Quality::Sdtv)
} else if caps.name("hdtv").is_some() {
Some(Quality::Hdtv720p)
} else {
None
};
if let Some(quality) = q {
return QualityModel {
quality,
revision,
source_detection_source: QualityDetectionSource::Name,
resolution_detection_source,
revision_detection_source,
};
}
}
QualityModel {
revision,
revision_detection_source,
resolution_detection_source,
..Default::default()
}
}
pub fn parse_quality(name: &str) -> crate::quality::QualityModel {
use crate::quality::{Quality, QualityDetectionSource};
let trimmed = name.trim();
let mut result = parse_quality_name(trimmed);
if result.quality == Quality::Unknown && !contains_invalid_path_chars(trimmed) {
let ext = get_path_extension(trimmed);
result.source_detection_source = QualityDetectionSource::Extension;
result.resolution_detection_source = QualityDetectionSource::Extension;
result.quality = quality_for_extension(ext);
}
result
}
fn contains_invalid_path_chars(text: &str) -> bool {
text.bytes().any(|b| b == 0 || (0x01..=0x1F).contains(&b))
}
fn parse_quality_modifiers(
name: &str,
normalized_name: &str,
) -> (
crate::quality::Revision,
crate::quality::QualityDetectionSource,
) {
use crate::quality::{QualityDetectionSource, Revision};
let mut rev = Revision::default();
let mut detection = QualityDetectionSource::Unknown;
let version_caps = VERSION_REGEX.captures(normalized_name);
let version_value: Option<u32> = version_caps.as_ref().and_then(|caps| {
caps.name("version")
.or_else(|| caps.name("version2"))
.or_else(|| caps.name("version3"))
.or_else(|| caps.name("version4"))
.or_else(|| caps.name("version5"))
.and_then(|m| m.as_str().parse().ok())
});
if let Some(v) = version_value {
rev.version = v;
detection = QualityDetectionSource::Name;
}
if PROPER_REGEX.is_match(normalized_name) {
rev.version = match version_value {
Some(v) => v + 1,
None => 2,
};
detection = QualityDetectionSource::Name;
}
if REPACK_REGEX.is_match(normalized_name) {
rev.version = match version_value {
Some(v) => v + 1,
None => 2,
};
rev.is_repack = true;
detection = QualityDetectionSource::Name;
}
let real_count = REAL_REGEX.find_iter(name).count();
if real_count > 0 {
rev.real = u32::try_from(real_count).unwrap_or(u32::MAX);
detection = QualityDetectionSource::Name;
}
(rev, detection)
}
#[cfg(test)]
mod tests {
use super::regexes::*;
use super::{
SourceGroup, detect_resolution, match_remux, match_source, match_source_last,
matches_alternative_resolution, matches_anime_bluray, parse_quality, parse_quality_name,
};
use crate::quality::{Quality, QualityDetectionSource, Resolution};
#[test]
fn source_regex_detects_bluray() {
let m = SOURCE_REGEX
.captures("Movie.2020.1080p.BluRay.x264")
.unwrap();
assert!(m.name("bluray").is_some());
}
#[test]
fn source_regex_detects_webdl() {
let m = SOURCE_REGEX
.captures("Show.S01E01.WEB-DL.AAC.x264")
.unwrap();
assert!(m.name("webdl").is_some());
}
#[test]
fn source_regex_detects_webrip() {
let m = SOURCE_REGEX.captures("Show.S01E01.WEBRip.x264").unwrap();
assert!(m.name("webrip").is_some());
}
#[test]
fn source_regex_detects_hdtv() {
let m = SOURCE_REGEX.captures("Show.S01E01.HDTV.x264").unwrap();
assert!(m.name("hdtv").is_some());
}
#[test]
fn source_regex_detects_dvd() {
let m = SOURCE_REGEX.captures("Movie.2010.DVDRip.XviD").unwrap();
assert!(m.name("dvd").is_some());
}
#[test]
fn match_source_rejects_bare_bd_at_end_of_string() {
let result = match_source("Show.S01E01.BD");
if let Some(found) = result {
assert_ne!(
found.group,
SourceGroup::Bluray,
"bare BD at EOL should not match bluray; got matched={:?}",
found.matched
);
}
}
#[test]
fn match_source_accepts_bd_when_followed_by_more() {
let m = match_source("Show.BD.x264.somegroup").expect("should match bluray");
assert_eq!(m.group, SourceGroup::Bluray);
assert_eq!(m.matched, "BD");
}
#[test]
fn match_source_accepts_bdmux_at_eol() {
let m = match_source("Show.BDMux").expect("BDMux at EOL must match bluray");
assert_eq!(m.group, SourceGroup::Bluray);
}
#[test]
fn match_source_rejects_amzn_web_rip() {
let result = match_source("Show.S01E01.AMZN.WEB.Rip.x264");
if let Some(found) = result {
assert_ne!(
found.group,
SourceGroup::Webdl,
"AMZN.WEB.Rip must not be classified as webdl; got matched={:?}",
found.matched
);
}
}
#[test]
fn match_source_accepts_amzn_web_dl() {
let m = match_source("Show.S01E01.AMZN.WEB-DL.x264").expect("AMZN.WEB-DL must match webdl");
assert_eq!(m.group, SourceGroup::Webdl);
assert_eq!(
m.matched.to_ascii_uppercase(),
"AMZN.WEB-",
"expected provider_web branch (AMZN.WEB-), got `{}`",
m.matched
);
}
#[test]
fn match_source_accepts_amzn_web_when_not_followed_by_rip() {
let m = match_source("Show.S01E01.AMZN.WEB.h264").expect("AMZN.WEB.h264 must match webdl");
assert_eq!(m.group, SourceGroup::Webdl);
}
#[test]
fn source_regex_uppercase_web_at_eol_matches_webdl() {
let m = SOURCE_REGEX.captures("Show.S01E01.WEB").unwrap();
assert!(
m.name("webdl").is_some(),
"uppercase WEB at EOL must match webdl"
);
}
#[test]
fn source_regex_lowercase_web_at_eol_does_not_match_via_eol_branch() {
let result = SOURCE_REGEX.captures("Show.S01E01.web");
if let Some(c) = result {
assert!(
c.name("webdl").is_none(),
"lowercase web at EOL must not match webdl"
);
}
}
#[test]
fn raw_hd_regex_detects_rawhd() {
assert!(RAW_HD_REGEX.is_match("Show.S01E01.RawHD.MPEG2"));
}
#[test]
fn raw_hd_regex_detects_raw_hyphen_hd() {
assert!(RAW_HD_REGEX.is_match("Show.S01E01.Raw-HD"));
}
#[test]
fn raw_hd_regex_does_not_match_unrelated() {
assert!(!RAW_HD_REGEX.is_match("Show.S01E01.RAWisCool"));
}
#[test]
fn mpeg2_regex_detects_mpeg2() {
assert!(MPEG2_REGEX.is_match("Movie.MPEG2.x264"));
}
#[test]
fn mpeg2_regex_detects_mpeg_dash_2() {
assert!(MPEG2_REGEX.is_match("Movie.MPEG-2.x264"));
}
#[test]
fn mpeg2_regex_is_case_sensitive() {
assert!(!MPEG2_REGEX.is_match("Movie.mpeg2.x264"));
}
#[test]
fn remux_regex_detects_uhd_remux() {
assert!(REMUX_REGEX.is_match("Movie.2020.UHD.Remux.2160p"));
}
#[test]
fn remux_regex_detects_bd_remux_prefix() {
assert!(REMUX_REGEX.is_match("Movie.2020.1080p-Remux"));
}
#[test]
fn remux_regex_detects_remux_then_resolution() {
assert!(REMUX_REGEX.is_match("Movie.2020.Remux_2160p"));
}
#[test]
fn match_remux_returns_text() {
let s = "Movie.2020.UHD.Remux.2160p";
let got = match_remux(s).expect("must match");
assert!(got.to_ascii_lowercase().contains("remux"));
}
#[test]
fn anime_bluray_regex_detects_bd_with_resolution() {
assert!(matches_anime_bluray("[group] Show - 01 [BD1080p]"));
}
#[test]
fn anime_bluray_regex_detects_bd_in_brackets() {
assert!(matches_anime_bluray("[group] Show - 01 [BD]"));
}
#[test]
fn anime_bluray_regex_detects_bd_with_space() {
assert!(matches_anime_bluray("[group] Show - 01 [BD 1080p]"));
}
#[test]
fn anime_bluray_regex_does_not_match_substring() {
assert!(!matches_anime_bluray("Some.abdef.movie"));
}
#[test]
fn anime_bluray_regex_does_not_match_bd_then_letter() {
assert!(!matches_anime_bluray("Show.bdrip"));
}
#[test]
fn anime_bluray_regex_rejects_bare_bd_at_sol() {
assert!(!matches_anime_bluray("bd"));
assert!(!matches_anime_bluray("bd.x264"));
}
#[test]
fn anime_bluray_regex_rejects_bare_bd_at_eol() {
assert!(!matches_anime_bluray("show.bd"));
}
#[test]
fn anime_bluray_regex_accepts_bd1080_at_boundaries() {
assert!(matches_anime_bluray("bd1080"));
assert!(matches_anime_bluray("bd1080.mkv"));
}
#[test]
fn anime_webdl_regex_detects_web_brackets() {
assert!(ANIME_WEBDL_REGEX.is_match("[group] Show - 01 [WEB] [1080p]"));
}
#[test]
fn anime_webdl_regex_detects_paren_web_dot() {
assert!(ANIME_WEBDL_REGEX.is_match("(WEB.1080p) Show"));
}
#[test]
fn anime_webdl_regex_does_not_match_bare_web() {
assert!(!ANIME_WEBDL_REGEX.is_match("Show.WEB.1080p"));
}
#[test]
fn resolution_regex_detects_2160p() {
let m = RESOLUTION_REGEX.captures("Movie.2160p.HDR").unwrap();
assert!(m.name("R2160p").is_some());
}
#[test]
fn resolution_regex_detects_1280x720_as_720p() {
let m = RESOLUTION_REGEX
.captures("[group] Show - 01 (1280x720)")
.unwrap();
assert!(m.name("R720p").is_some());
}
#[test]
fn resolution_regex_detects_uhd_4k_form() {
let m = RESOLUTION_REGEX.captures("Movie.2020.UHD-4k.x265").unwrap();
assert!(m.name("R2160p").is_some());
}
#[test]
fn resolution_regex_detects_4k_uhd_form() {
let m = RESOLUTION_REGEX.captures("Movie.2020.4k-UHD.x265").unwrap();
assert!(m.name("R2160p").is_some());
}
#[test]
fn resolution_regex_detects_480p() {
let m = RESOLUTION_REGEX.captures("Show.S01E01.480p.x264").unwrap();
assert!(m.name("R480p").is_some());
}
#[test]
fn resolution_regex_detects_1080p_fhd() {
let m = RESOLUTION_REGEX.captures("Movie.2020.FHD.x264").unwrap();
assert!(m.name("R1080p").is_some());
}
#[test]
fn resolution_regex_detects_4kto1080p_form() {
let m = RESOLUTION_REGEX.captures("Movie.4kto1080p.x264").unwrap();
assert!(
m.name("R1080p").is_some(),
"expected R1080p branch to match `4kto1080p`"
);
}
#[test]
fn alternative_resolution_regex_matches_uhd() {
assert!(matches_alternative_resolution("Movie.UHD.Bluray"));
}
#[test]
fn alternative_resolution_regex_matches_bracket_4k() {
assert!(matches_alternative_resolution("Movie [4K] Bluray"));
}
#[test]
fn alternative_resolution_regex_does_not_match_unrelated() {
assert!(!matches_alternative_resolution("Movie.1080p.Bluray"));
}
#[test]
fn alternative_resolution_regex_named_groups_are_distinct() {
let uhd = ALTERNATIVE_RESOLUTION_REGEX
.captures("Movie.UHD.x264")
.unwrap();
assert!(uhd.name("R2160p").is_some());
assert!(uhd.name("R2160p_alt").is_none());
let four_k = ALTERNATIVE_RESOLUTION_REGEX
.captures("Movie [4K] x264")
.unwrap();
assert!(four_k.name("R2160p").is_none());
assert!(four_k.name("R2160p_alt").is_some());
}
#[test]
fn codec_regex_detects_xvid() {
let m = CODEC_REGEX.captures("Show.S01E01.Xvid.AC3").unwrap();
assert!(m.name("xvid").is_some());
}
#[test]
fn codec_regex_does_not_match_x_hyphen_vid() {
assert!(CODEC_REGEX.captures("Movie.X-vid.AC3").is_none());
}
#[test]
fn codec_regex_detects_x264() {
let m = CODEC_REGEX.captures("Movie.1080p.x264.AC3").unwrap();
assert!(m.name("x264").is_some());
}
#[test]
fn codec_regex_detects_h264_case_insensitive() {
let m = CODEC_REGEX.captures("Movie.1080p.H264.AC3").unwrap();
assert!(m.name("h264").is_some());
}
#[test]
fn codec_regex_detects_xvidhd() {
let m = CODEC_REGEX.captures("Movie.XvidHD.AC3").unwrap();
assert!(m.name("xvidhd").is_some());
assert!(m.name("xvid").is_none());
}
#[test]
fn codec_regex_detects_divx() {
let m = CODEC_REGEX.captures("Movie.divx.AC3").unwrap();
assert!(m.name("divx").is_some());
}
#[test]
fn other_source_regex_detects_hd_tv() {
let m = OTHER_SOURCE_REGEX.captures("Show.HD-TV.x264").unwrap();
assert!(m.name("hdtv").is_some());
}
#[test]
fn other_source_regex_detects_sd_tv() {
let m = OTHER_SOURCE_REGEX.captures("Show.SD.TV.x264").unwrap();
assert!(m.name("sdtv").is_some());
}
#[test]
fn high_def_pdtv_regex_detects_hr_ws() {
assert!(HIGH_DEF_PDTV_REGEX.is_match("Show.S01E01.hr-ws.x264"));
}
#[test]
fn high_def_pdtv_regex_does_not_match_unrelated() {
assert!(!HIGH_DEF_PDTV_REGEX.is_match("Show.S01E01.hrws.x264"));
}
#[test]
fn proper_regex_detects_proper() {
assert!(PROPER_REGEX.is_match("Show PROPER 720p"));
}
#[test]
fn proper_regex_detects_proper_lowercase() {
assert!(PROPER_REGEX.is_match("Show.proper.720p"));
}
#[test]
fn repack_regex_detects_repack() {
assert!(REPACK_REGEX.is_match("Show REPACK2 1080p"));
}
#[test]
fn repack_regex_detects_rerip() {
assert!(REPACK_REGEX.is_match("Show.RERIP.1080p"));
}
#[test]
fn repack_regex_detects_bare_repack() {
assert!(REPACK_REGEX.is_match("Show.REPACK.1080p"));
}
#[test]
fn version_regex_detects_v2() {
let m = VERSION_REGEX.captures("Show 1080p v2").unwrap();
let version = m
.name("version")
.or_else(|| m.name("version2"))
.or_else(|| m.name("version3"))
.or_else(|| m.name("version4"))
.or_else(|| m.name("version5"))
.expect("at least one named version group must capture");
assert_eq!(version.as_str(), "2");
}
#[test]
fn version_regex_detects_bracket_v3() {
let m = VERSION_REGEX.captures("Show [v3] 1080p").unwrap();
assert!(m.name("version2").is_some());
assert_eq!(m.name("version2").unwrap().as_str(), "3");
}
#[test]
fn version_regex_detects_repack_with_digit() {
let m = VERSION_REGEX.captures("Show.repack2.1080p").unwrap();
assert!(m.name("version3").is_some());
assert_eq!(m.name("version3").unwrap().as_str(), "2");
}
#[test]
fn version_regex_detects_rerip_with_digit() {
let m = VERSION_REGEX.captures("Show.rerip3.1080p").unwrap();
assert!(m.name("version4").is_some());
assert_eq!(m.name("version4").unwrap().as_str(), "3");
}
#[test]
fn version_regex_detects_digit_v_digit_form() {
let m = VERSION_REGEX.captures("Show 01-v2_1080p").unwrap();
assert!(m.name("version").is_some());
assert_eq!(m.name("version").unwrap().as_str(), "2");
}
#[test]
fn real_regex_detects_real() {
assert!(REAL_REGEX.is_match("Show REAL PROPER 1080p"));
}
#[test]
fn real_regex_is_case_sensitive() {
assert!(!REAL_REGEX.is_match("show real proper"));
assert!(!REAL_REGEX.is_match("Show Real Proper"));
}
#[test]
fn parses_proper_increments_revision() {
let m = parse_quality_name("Show S01E01 PROPER HDTV XviD");
assert_eq!(m.revision.version, 2);
}
#[test]
fn parses_repack_sets_is_repack() {
let m = parse_quality_name("Show.2010.REPACK.1080p.WEB");
assert!(m.revision.is_repack);
assert_eq!(m.revision.version, 2);
}
#[test]
fn parses_real_sets_real_flag() {
let m = parse_quality_name("Show.REAL.PROPER.1080p");
assert_eq!(m.revision.real, 1);
assert_eq!(m.revision.version, 2);
}
#[test]
fn parses_explicit_version_2() {
let m = parse_quality_name("Show.S01E01.1080p.v2");
assert_eq!(m.revision.version, 2);
}
#[test]
fn raw_hd_short_circuits() {
let m = parse_quality_name("Show.S01E01.RawHD");
assert_eq!(m.quality, Quality::RawHd);
}
#[test]
fn parse_quality_name_default_when_no_modifiers_or_rawhd() {
let m = parse_quality_name("Show.S01E01.MysteryFormat");
assert_eq!(m.quality, Quality::Unknown);
assert_eq!(m.revision.version, 1);
assert_eq!(m.revision.real, 0);
assert!(!m.revision.is_repack);
}
#[test]
fn parse_quality_name_empty_input_returns_default() {
let m = parse_quality_name("");
assert_eq!(m.quality, Quality::Unknown);
assert_eq!(m.revision.version, 1);
assert_eq!(m.revision.real, 0);
assert!(!m.revision.is_repack);
}
#[test]
fn parse_quality_name_normalizes_underscores() {
let m = parse_quality_name("Show_S01E01_PROPER_HDTV");
assert_eq!(m.revision.version, 2);
}
#[test]
fn parse_quality_name_real_uses_raw_input_not_normalized() {
let m = parse_quality_name("Show.REAL.S01E01.1080p");
assert_eq!(m.revision.real, 1);
}
#[test]
fn parse_quality_name_real_underscore_bordered_does_not_match() {
let m = parse_quality_name("Show_S01E01_REAL_1080p");
assert_eq!(m.revision.real, 0);
}
#[test]
fn parse_quality_name_real_lowercase_is_ignored() {
let m = parse_quality_name("Show.real.S01E01.1080p");
assert_eq!(m.revision.real, 0);
}
#[test]
fn parse_quality_name_multi_real_counts() {
let m = parse_quality_name("Show.REAL.REAL.1080p");
assert_eq!(m.revision.real, 2);
}
#[test]
fn parse_quality_name_repack_with_digit_takes_version() {
let m = parse_quality_name("Show.repack2.1080p");
assert_eq!(m.revision.version, 3);
assert!(m.revision.is_repack);
}
#[test]
fn parse_quality_name_proper_with_digit_v2_takes_version() {
let m = parse_quality_name("Show.PROPER.1080p.v2");
assert_eq!(m.revision.version, 3);
}
#[test]
fn raw_hd_short_circuits_populates_detection_sources() {
let m = parse_quality_name("Show.S01E01.RawHD");
assert_eq!(
m.source_detection_source,
crate::quality::QualityDetectionSource::Name
);
assert_eq!(
m.resolution_detection_source,
crate::quality::QualityDetectionSource::Name
);
assert_eq!(
m.revision_detection_source,
crate::quality::QualityDetectionSource::Unknown
);
}
#[test]
fn raw_hd_short_circuits_with_proper_sets_revision_detection_source() {
let m = parse_quality_name("Show.RawHD.PROPER");
assert_eq!(m.quality, Quality::RawHd);
assert_eq!(m.revision.version, 2);
assert_eq!(
m.revision_detection_source,
crate::quality::QualityDetectionSource::Name
);
}
#[test]
fn detect_resolution_returns_2160p_for_2160p_token() {
assert_eq!(
detect_resolution("Movie.2020.2160p.BluRay"),
Resolution::R2160p
);
}
#[test]
fn detect_resolution_returns_2160p_for_uhd_token() {
assert_eq!(
detect_resolution("Movie.2020.UHD.BluRay"),
Resolution::R2160p
);
}
#[test]
fn detect_resolution_returns_2160p_for_bracket_4k() {
assert_eq!(
detect_resolution("Movie.2020 [4K] BluRay"),
Resolution::R2160p
);
}
#[test]
fn detect_resolution_returns_1080p_for_1080p_token() {
assert_eq!(
detect_resolution("Movie.2020.1080p.WEB-DL"),
Resolution::R1080p
);
}
#[test]
fn detect_resolution_returns_720p_for_1280x720_token() {
assert_eq!(detect_resolution("Show.S01E01.1280x720"), Resolution::R720p);
}
#[test]
fn detect_resolution_returns_480p_for_480p_token() {
assert_eq!(
detect_resolution("Show.S01E01.480p.HDTV"),
Resolution::R480p
);
}
#[test]
fn detect_resolution_returns_540p_for_540p_token() {
assert_eq!(
detect_resolution("Show.S01E01.540p.HDTV"),
Resolution::R540p
);
}
#[test]
fn detect_resolution_returns_576p_for_576p_token() {
assert_eq!(
detect_resolution("Show.S01E01.576p.HDTV"),
Resolution::R576p
);
}
#[test]
fn detect_resolution_returns_360p_for_360p_token() {
assert_eq!(
detect_resolution("Show.S01E01.360p.HDTV"),
Resolution::R360p
);
}
#[test]
fn detect_resolution_returns_unknown_when_no_token() {
assert_eq!(
detect_resolution("Show.S01E01.HDTV.x264"),
Resolution::Unknown
);
}
#[test]
fn match_source_last_returns_last_when_two_distinct_sources() {
let m = match_source_last("Show.HDTV.Repack.1080p.BluRay.x264").expect("BluRay must match");
assert_eq!(m.group, SourceGroup::Bluray);
}
#[test]
fn match_source_last_returns_first_when_only_one_source() {
let m = match_source_last("Movie.2020.1080p.BluRay.x264").expect("BluRay must match");
assert_eq!(m.group, SourceGroup::Bluray);
}
#[test]
fn match_source_last_skips_filtered_candidates() {
assert!(match_source_last("Show.S01E01.BD").is_none());
}
#[test]
fn bluray_2160p() {
assert_eq!(
parse_quality_name("Movie.2020.2160p.BluRay.x265").quality,
Quality::Bluray2160p
);
}
#[test]
fn bluray_1080p_remux() {
assert_eq!(
parse_quality_name("Movie.2020.1080p.BluRay.Remux.AVC").quality,
Quality::Bluray1080pRemux
);
}
#[test]
fn bluray_720p() {
assert_eq!(
parse_quality_name("Movie.720p.BluRay.x264").quality,
Quality::Bluray720p
);
}
#[test]
fn bluray_576p() {
assert_eq!(
parse_quality_name("Movie.576p.BluRay.x264").quality,
Quality::Bluray576p
);
}
#[test]
fn bluray_480p_via_xvid_codec() {
assert_eq!(
parse_quality_name("Movie.2020.BluRay.Xvid.AC3").quality,
Quality::Bluray480p
);
}
#[test]
fn bluray_480p_via_divx_codec() {
assert_eq!(
parse_quality_name("Movie.2020.BluRay.divx.AC3").quality,
Quality::Bluray480p
);
}
#[test]
fn bluray_480p_via_low_resolution() {
assert_eq!(
parse_quality_name("Movie.2020.480p.BluRay.x264").quality,
Quality::Bluray480p
);
assert_eq!(
parse_quality_name("Movie.2020.540p.BluRay.x264").quality,
Quality::Bluray480p
);
}
#[test]
fn bluray_2160p_remux_via_2160p_token_and_remux() {
assert_eq!(
parse_quality_name("Movie.2020.2160p.BluRay.Remux.HDR.x265").quality,
Quality::Bluray2160pRemux
);
}
#[test]
fn bluray_remux_unknown_resolution_falls_back_to_1080p_remux() {
assert_eq!(
parse_quality_name("Movie.BluRay.Remux.x264").quality,
Quality::Bluray1080pRemux
);
}
#[test]
fn bluray_720p_with_remux_stays_720p() {
assert_eq!(
parse_quality_name("Movie.720p.BluRay.Remux.x264").quality,
Quality::Bluray720p
);
}
#[test]
fn bluray_unknown_resolution_no_remux_falls_back_to_720p() {
assert_eq!(
parse_quality_name("Movie.BluRay.x264").quality,
Quality::Bluray720p
);
}
#[test]
fn webdl_1080p() {
assert_eq!(
parse_quality_name("Movie.2020.1080p.WEB-DL.x264").quality,
Quality::Webdl1080p
);
}
#[test]
fn webdl_720p_itunes() {
assert_eq!(
parse_quality_name("Movie.720p.iTunesHD.AVC").quality,
Quality::Webdl720p
);
}
#[test]
fn webdl_2160p() {
assert_eq!(
parse_quality_name("Movie.2020.2160p.WEB-DL.x265").quality,
Quality::Webdl2160p
);
}
#[test]
fn webdl_480p_default() {
assert_eq!(
parse_quality_name("Movie.2020.WEB-DL.AAC.x264").quality,
Quality::Webdl480p
);
}
#[test]
fn webdl_720p_via_bracket_marker() {
assert_eq!(
parse_quality_name("Movie.[WEBDL].WEB-DL.AC3").quality,
Quality::Webdl720p
);
}
#[test]
fn webrip_2160p() {
assert_eq!(
parse_quality_name("Movie.2160p.WebRip.x265").quality,
Quality::Webrip2160p
);
}
#[test]
fn webrip_1080p() {
assert_eq!(
parse_quality_name("Movie.2020.1080p.WEBRip.x264").quality,
Quality::Webrip1080p
);
}
#[test]
fn webrip_720p() {
assert_eq!(
parse_quality_name("Show.S01E01.720p.WEBRip.x264").quality,
Quality::Webrip720p
);
}
#[test]
fn webrip_480p_default() {
assert_eq!(
parse_quality_name("Show.S01E01.WEBRip.x264").quality,
Quality::Webrip480p
);
}
#[test]
fn hdtv_720p() {
assert_eq!(
parse_quality_name("Show.S01E01.720p.HDTV.x264").quality,
Quality::Hdtv720p
);
}
#[test]
fn hdtv_1080p() {
assert_eq!(
parse_quality_name("Show.S01E01.1080p.HDTV.x264").quality,
Quality::Hdtv1080p
);
}
#[test]
fn hdtv_2160p() {
assert_eq!(
parse_quality_name("Show.S01E01.2160p.HDTV.x265").quality,
Quality::Hdtv2160p
);
}
#[test]
fn hdtv_mpeg2_short_circuits_to_rawhd() {
assert_eq!(
parse_quality_name("Show.S01E01.1080p.HDTV.MPEG2").quality,
Quality::RawHd
);
}
#[test]
fn hdtv_mpeg2_lowercase_does_not_short_circuit() {
assert_eq!(
parse_quality_name("Show.S01E01.1080p.HDTV.mpeg2.x264").quality,
Quality::Hdtv1080p
);
}
#[test]
fn hdtv_720p_via_bracket_marker() {
assert_eq!(
parse_quality_name("Show.S01E01.[HDTV].x264").quality,
Quality::Hdtv720p
);
}
#[test]
fn dvd() {
assert_eq!(
parse_quality_name("Movie.2010.DVDRip.XviD-AAC").quality,
Quality::Dvd
);
}
#[test]
fn dvd_with_explicit_dvd_token() {
assert_eq!(
parse_quality_name("Movie.2010.DVD.XviD-AAC").quality,
Quality::Dvd
);
}
#[test]
fn dvd_ntsc_token() {
assert_eq!(
parse_quality_name("Movie.2010.NTSC.XviD-AAC").quality,
Quality::Dvd
);
}
#[test]
fn bdrip_720p_maps_to_bluray720p() {
assert_eq!(
parse_quality_name("Movie.720p.BDRip.x264").quality,
Quality::Bluray720p
);
}
#[test]
fn bdrip_1080p_maps_to_bluray1080p() {
assert_eq!(
parse_quality_name("Movie.1080p.BDRip.x264").quality,
Quality::Bluray1080p
);
}
#[test]
fn bdrip_default_maps_to_bluray480p() {
assert_eq!(
parse_quality_name("Movie.BDRip.x264").quality,
Quality::Bluray480p
);
}
#[test]
fn brrip_2160p_maps_to_bluray2160p() {
assert_eq!(
parse_quality_name("Movie.2160p.BRRip.x265").quality,
Quality::Bluray2160p
);
}
#[test]
fn sdtv_default() {
assert_eq!(
parse_quality_name("Show.S01E01.HDTV.x264").quality,
Quality::Sdtv
);
}
#[test]
fn pdtv_no_resolution_maps_to_sdtv() {
assert_eq!(
parse_quality_name("Show.S01E01.PDTV.x264").quality,
Quality::Sdtv
);
}
#[test]
fn pdtv_1080p_maps_to_hdtv1080p() {
assert_eq!(
parse_quality_name("Show.S01E01.1080p.PDTV.x264").quality,
Quality::Hdtv1080p
);
}
#[test]
fn pdtv_720p_maps_to_hdtv720p() {
assert_eq!(
parse_quality_name("Show.S01E01.720p.PDTV.x264").quality,
Quality::Hdtv720p
);
}
#[test]
fn pdtv_hr_ws_maps_to_hdtv720p() {
let m = parse_quality_name("Show.S01E01.PDTV.hr-ws.x264");
assert_eq!(m.quality, Quality::Hdtv720p);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn sdtv_branch_with_explicit_sdtv_token() {
assert_eq!(
parse_quality_name("Show.S01E01.SDTV.x264").quality,
Quality::Sdtv
);
}
#[test]
fn dsr_branch_default_sdtv() {
assert_eq!(
parse_quality_name("Show.S01E01.DSR.x264").quality,
Quality::Sdtv
);
}
#[test]
fn tvrip_branch_default_sdtv() {
assert_eq!(
parse_quality_name("Show.S01E01.TVRip.x264").quality,
Quality::Sdtv
);
}
#[test]
fn cascade_uses_last_source_match() {
let m = parse_quality_name("Show.HDTV.Repack.1080p.BluRay.x264");
assert_eq!(m.quality, Quality::Bluray1080p);
}
#[test]
fn cascade_populates_source_detection_when_source_fires() {
let m = parse_quality_name("Movie.1080p.BluRay.x264");
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
}
#[test]
fn cascade_populates_resolution_detection_when_resolution_fires() {
let m = parse_quality_name("Movie.1080p.BluRay.x264");
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn cascade_leaves_resolution_detection_unknown_when_no_resolution() {
let m = parse_quality_name("Movie.WEB-DL.x264");
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
assert_eq!(
m.resolution_detection_source,
QualityDetectionSource::Unknown
);
}
#[test]
fn cascade_falls_back_to_unknown_when_no_source() {
let m = parse_quality_name("Show.S01E01.MysteryFormat");
assert_eq!(m.quality, Quality::Unknown);
}
#[test]
fn cascade_resolution_only_1080p_maps_to_hdtv1080p() {
let m = parse_quality_name("Movie.2020.1080p.x264");
assert_eq!(m.quality, Quality::Hdtv1080p);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn cascade_underscore_normalised_for_source_match() {
let m = parse_quality_name("Movie_2020_1080p_BluRay_x264");
assert_eq!(m.quality, Quality::Bluray1080p);
}
#[test]
fn cascade_trims_outer_whitespace() {
let m = parse_quality_name(" Movie.2020.1080p.BluRay.x264 ");
assert_eq!(m.quality, Quality::Bluray1080p);
}
#[test]
fn cascade_revision_carries_through_arm() {
let m = parse_quality_name("Movie.2020.1080p.BluRay.PROPER.x264");
assert_eq!(m.quality, Quality::Bluray1080p);
assert_eq!(m.revision.version, 2);
assert_eq!(m.revision_detection_source, QualityDetectionSource::Name);
}
#[test]
fn sourceless_remux_2160p_maps_to_bluray2160p_remux() {
let m = parse_quality_name("Movie.2020.2160p.Remux.HDR.x265");
assert_eq!(m.quality, Quality::Bluray2160pRemux);
assert_eq!(m.source_detection_source, QualityDetectionSource::Unknown);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn sourceless_remux_1080p_maps_to_bluray1080p_remux() {
let m = parse_quality_name("Movie.2020.1080p.Remux.AVC.DTS-HD");
assert_eq!(m.quality, Quality::Bluray1080pRemux);
assert_eq!(m.source_detection_source, QualityDetectionSource::Unknown);
}
#[test]
fn sourceless_remux_720p_maps_to_bluray720p() {
let m = parse_quality_name("Movie.2020.720p.Remux.AVC");
assert_eq!(m.quality, Quality::Bluray720p);
}
#[test]
fn sourceless_remux_480p_maps_to_bluray480p() {
let m = parse_quality_name("Movie.2020.480p.Remux.x264");
assert_eq!(m.quality, Quality::Bluray480p);
}
#[test]
fn anime_bluray_720p() {
let m = parse_quality_name("[group] Show - 01 [BD720p]");
assert_eq!(m.quality, Quality::Bluray720p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
}
#[test]
fn anime_bluray_1080p() {
let m = parse_quality_name("[group] Show - 01 [BD1080p]");
assert_eq!(m.quality, Quality::Bluray1080p);
}
#[test]
fn anime_bluray_2160p() {
let m = parse_quality_name("[group] Show - 01 [BD2160p]");
assert_eq!(m.quality, Quality::Bluray2160p);
}
#[test]
fn anime_bluray_480p_maps_to_dvd() {
let m = parse_quality_name("Show.480p.bd720.AC3");
assert_eq!(m.quality, Quality::Dvd);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn anime_bluray_remux_no_resolution_falls_back_to_1080p_remux() {
let m = parse_quality_name("[group] Show.(bd).Remux");
assert_eq!(m.quality, Quality::Bluray1080pRemux);
}
#[test]
fn anime_bluray_1080p_remux_combo() {
let m = parse_quality_name("[group] Show.1080p.(bd).Remux");
assert_eq!(m.quality, Quality::Bluray1080pRemux);
}
#[test]
fn anime_bluray_unknown_resolution_no_remux_defaults_720p() {
let m = parse_quality_name("[group] Show.(bd).x264");
assert_eq!(m.quality, Quality::Bluray720p);
}
#[test]
fn anime_webdl_1080p() {
let m = parse_quality_name("[group] Show - 01 [WEB][1080p]");
assert_eq!(m.quality, Quality::Webdl1080p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
}
#[test]
fn anime_webdl_2160p() {
let m = parse_quality_name("[group] Show - 01 [WEB][2160p]");
assert_eq!(m.quality, Quality::Webdl2160p);
}
#[test]
fn anime_webdl_480p() {
let m = parse_quality_name("[group] Show - 01 [WEB][480p]");
assert_eq!(m.quality, Quality::Webdl480p);
}
#[test]
fn anime_webdl_unknown_resolution_defaults_720p() {
let m = parse_quality_name("[group] Show - 01 (WEB.)");
assert_eq!(m.quality, Quality::Webdl720p);
}
#[test]
fn resolution_only_2160p_maps_to_hdtv2160p() {
let m = parse_quality_name("Movie.2020.2160p.x265");
assert_eq!(m.quality, Quality::Hdtv2160p);
}
#[test]
fn resolution_only_720p_maps_to_hdtv720p() {
let m = parse_quality_name("Movie.2020.720p.x264");
assert_eq!(m.quality, Quality::Hdtv720p);
}
#[test]
fn resolution_only_480p_maps_to_sdtv() {
let m = parse_quality_name("Movie.2020.480p.x264");
assert_eq!(m.quality, Quality::Sdtv);
}
#[test]
fn resolution_only_with_mkv_extension_promotes_via_television_source() {
let m = parse_quality_name("Movie.2020.1080p.x264.mkv");
assert_eq!(m.quality, Quality::Hdtv1080p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Extension);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn resolution_only_with_iso_extension_promotes_to_dvd_at_480p() {
let m = parse_quality_name("Movie.2020.480p.x264.iso");
assert_eq!(m.quality, Quality::Dvd);
assert_eq!(m.source_detection_source, QualityDetectionSource::Extension);
}
#[test]
fn x264_codec_alone_maps_to_sdtv() {
let m = parse_quality_name("Movie.x264.AC3");
assert_eq!(m.quality, Quality::Sdtv);
}
#[test]
fn pixel_tag_848x480_with_dvd_token_no_word_boundary() {
let m = parse_quality_name("Movie848x480dvd");
assert_eq!(m.quality, Quality::Dvd);
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn pixel_tag_848x480_with_bluray_token_no_word_boundary() {
let m = parse_quality_name("Movie848x480bluray");
assert_eq!(m.quality, Quality::Bluray480p);
}
#[test]
fn pixel_tag_848x480_default_sdtv() {
let m = parse_quality_name("Movie848x480extra");
assert_eq!(m.quality, Quality::Sdtv);
}
#[test]
fn pixel_tag_1280x720_default_hdtv720p() {
let m = parse_quality_name("Movie1280x720extra");
assert_eq!(m.quality, Quality::Hdtv720p);
}
#[test]
fn pixel_tag_1280x720_with_bluray_token() {
let m = parse_quality_name("Movie1280x720bluray");
assert_eq!(m.quality, Quality::Bluray720p);
}
#[test]
fn pixel_tag_1920x1080_default_hdtv1080p() {
let m = parse_quality_name("Movie1920x1080extra");
assert_eq!(m.quality, Quality::Hdtv1080p);
}
#[test]
fn pixel_tag_1920x1080_with_bluray_token() {
let m = parse_quality_name("Movie1920x1080bluray");
assert_eq!(m.quality, Quality::Bluray1080p);
}
#[test]
fn bare_bluray720p_token() {
let m = parse_quality_name("Movie.bluray720p.AC3");
assert_eq!(m.quality, Quality::Bluray720p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
assert_eq!(m.resolution_detection_source, QualityDetectionSource::Name);
}
#[test]
fn bare_bluray1080p_token() {
let m = parse_quality_name("Movie.bluray1080p.AC3");
assert_eq!(m.quality, Quality::Bluray1080p);
}
#[test]
fn bare_bluray2160p_token() {
let m = parse_quality_name("Movie.bluray2160p.AC3");
assert_eq!(m.quality, Quality::Bluray2160p);
}
#[test]
fn other_source_hd_tv_maps_to_hdtv720p() {
let m = parse_quality_name("Show.HD-TV.AC3.foo");
assert_eq!(m.quality, Quality::Hdtv720p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
}
#[test]
fn other_source_sd_tv_maps_to_sdtv() {
let m = parse_quality_name("Show.SD-TV.AC3.foo");
assert_eq!(m.quality, Quality::Sdtv);
}
#[test]
fn extension_fallback_mkv_to_hdtv720p() {
let m = parse_quality("Show.S01E01.mkv");
assert_eq!(m.quality, Quality::Hdtv720p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Extension);
assert_eq!(
m.resolution_detection_source,
QualityDetectionSource::Extension
);
}
#[test]
fn extension_fallback_mp4_to_sdtv() {
let m = parse_quality("Show.S01E01.mp4");
assert_eq!(m.quality, Quality::Sdtv);
assert_eq!(m.source_detection_source, QualityDetectionSource::Extension);
}
#[test]
fn extension_fallback_iso_to_dvd() {
let m = parse_quality("Show.S01E01.iso");
assert_eq!(m.quality, Quality::Dvd);
}
#[test]
fn extension_fallback_m2ts_to_bluray720p() {
let m = parse_quality("Show.S01E01.m2ts");
assert_eq!(m.quality, Quality::Bluray720p);
}
#[test]
fn extension_fallback_webm_stays_unknown() {
let m = parse_quality("Show.S01E01.webm");
assert_eq!(m.quality, Quality::Unknown);
assert_eq!(m.source_detection_source, QualityDetectionSource::Extension);
}
#[test]
fn extension_fallback_unknown_extension_stays_unknown() {
let m = parse_quality("Show.S01E01.xyz");
assert_eq!(m.quality, Quality::Unknown);
assert_eq!(m.source_detection_source, QualityDetectionSource::Extension);
}
#[test]
fn extension_fallback_does_not_overwrite_resolved_quality() {
let m = parse_quality("Movie.2020.1080p.BluRay.x264.mp4");
assert_eq!(m.quality, Quality::Bluray1080p);
assert_eq!(m.source_detection_source, QualityDetectionSource::Name);
}
}