feat(indexer): tvsearch with season and episode tags (#78)
ci / web (push) Successful in 50s
ci / rust (push) Successful in 1m18s
e2e / e2e (push) Successful in 1m46s

This commit was merged in pull request #78.
This commit is contained in:
2026-08-22 22:34:04 +01:00
parent fc2a8de58d
commit 8d44225b57
11 changed files with 876 additions and 50 deletions
+164 -1
View File
@@ -8,7 +8,9 @@
//! pt-BR/pt-PT markers from §5.2, and real-world malformed names that
//! must parse partially rather than fail.
use arr_parse::{parse, Codec, Edition, HdrMarker, LanguageMarker, NameClaims, Resolution, Source};
use arr_parse::{
parse, Codec, Edition, EpisodeClaim, HdrMarker, LanguageMarker, NameClaims, Resolution, Source,
};
use serde as _;
struct Case {
@@ -21,6 +23,14 @@ fn s(v: &str) -> Option<String> {
Some(v.to_string())
}
#[allow(clippy::unnecessary_wraps)]
fn episodes(season: u32, episodes: &[u32]) -> Option<EpisodeClaim> {
Some(EpisodeClaim::Episodes {
season,
episodes: episodes.to_vec(),
})
}
/// Every resolution and every source tier appears at least once here,
/// pulled from a broad search rather than picked to fit.
#[allow(clippy::too_many_lines)]
@@ -56,6 +66,7 @@ fn matrix_cases() -> Vec<Case> {
source: Some(Source::WebDl),
codec: Some(Codec::X264),
group: s("RAWR"),
episode: episodes(2, &[1]),
..NameClaims::default()
},
},
@@ -67,6 +78,7 @@ fn matrix_cases() -> Vec<Case> {
source: Some(Source::Hdtv),
codec: Some(Codec::X264),
group: s("SYNCOPY"),
episode: episodes(8, &[14]),
..NameClaims::default()
},
},
@@ -78,6 +90,7 @@ fn matrix_cases() -> Vec<Case> {
source: Some(Source::BluRay),
codec: Some(Codec::X264),
group: s("CtrlHD"),
episode: episodes(3, &[22]),
..NameClaims::default()
},
},
@@ -229,6 +242,7 @@ fn matrix_cases() -> Vec<Case> {
source: Some(Source::WebDl),
codec: Some(Codec::X265),
group: s("NTb"),
episode: Some(EpisodeClaim::Season { season: 1 }),
..NameClaims::default()
},
},
@@ -268,6 +282,7 @@ fn matrix_cases() -> Vec<Case> {
source: Some(Source::Hdtv),
codec: Some(Codec::X264),
group: s("TJET"),
episode: Some(EpisodeClaim::Daily { year: 2026, month: 8, day: 21 }),
..NameClaims::default()
},
},
@@ -279,6 +294,7 @@ fn matrix_cases() -> Vec<Case> {
resolution: Some(Resolution::P480),
codec: Some(Codec::X264),
group: s("mSD"),
episode: episodes(8, &[37]),
..NameClaims::default()
},
},
@@ -291,6 +307,7 @@ fn matrix_cases() -> Vec<Case> {
resolution: Some(Resolution::P2160),
codec: Some(Codec::X265),
group: s("FaiLED"),
episode: episodes(1, &[1]),
..NameClaims::default()
},
},
@@ -399,6 +416,7 @@ fn pt_br_cases() -> Vec<Case> {
resolution: Some(Resolution::P1080),
source: Some(Source::Hdtv),
languages: vec![LanguageMarker::Portuguese, LanguageMarker::PtPt],
episode: Some(EpisodeClaim::Season { season: 1 }),
..NameClaims::default()
},
},
@@ -409,6 +427,7 @@ fn pt_br_cases() -> Vec<Case> {
resolution: Some(Resolution::P576),
source: Some(Source::WebDl),
languages: vec![LanguageMarker::Portuguese, LanguageMarker::PtPt],
episode: Some(EpisodeClaim::Season { season: 1 }),
..NameClaims::default()
},
},
@@ -471,6 +490,7 @@ fn multi_audio_cases() -> Vec<Case> {
codec: Some(Codec::X264),
languages: vec![LanguageMarker::Multi],
group: s("mSD"),
episode: episodes(4, &[18]),
..NameClaims::default()
},
},
@@ -484,6 +504,7 @@ fn multi_audio_cases() -> Vec<Case> {
codec: Some(Codec::X264),
languages: vec![LanguageMarker::Multi],
group: s("HiggsBoson"),
episode: episodes(1, &[1]),
..NameClaims::default()
},
},
@@ -523,6 +544,7 @@ fn multi_audio_cases() -> Vec<Case> {
hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr10Plus],
languages: vec![LanguageMarker::Dual],
group: s("Kitsune"),
episode: Some(EpisodeClaim::Season { season: 2 }),
..NameClaims::default()
},
},
@@ -723,6 +745,146 @@ fn malformed_cases() -> Vec<Case> {
year: Some(2003),
resolution: Some(Resolution::P480),
source: Some(Source::Remux),
episode: Some(EpisodeClaim::Season { season: 1 }),
..NameClaims::default()
},
},
]
}
/// Season and episode tags, the distinction grab selection turns on
/// (issue #37): single episodes, season packs, multi-episode files and
/// daily-dated shows.
#[allow(clippy::too_many_lines)]
fn tv_cases() -> Vec<Case> {
vec![
Case {
name: "The Rookie S08E14 720p HDTV x264-SYNCOPY".into(),
want: NameClaims {
title: s("The Rookie"),
resolution: Some(Resolution::P720),
source: Some(Source::Hdtv),
codec: Some(Codec::X264),
group: s("SYNCOPY"),
episode: episodes(8, &[14]),
..NameClaims::default()
},
},
Case {
name: "Slow.Horses.1x02.1080p.WEB-DL.DDP5.1.H.264-NTb".into(),
want: NameClaims {
title: s("Slow Horses"),
resolution: Some(Resolution::P1080),
source: Some(Source::WebDl),
codec: Some(Codec::X264),
group: s("NTb"),
episode: episodes(1, &[2]),
..NameClaims::default()
},
},
Case {
name: "Severance.S02.2160p.ATVP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX".into(),
want: NameClaims {
title: s("Severance"),
resolution: Some(Resolution::P2160),
source: Some(Source::WebDl),
codec: Some(Codec::X265),
hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr],
group: s("FLUX"),
episode: Some(EpisodeClaim::Season { season: 2 }),
..NameClaims::default()
},
},
Case {
name: "The Bear Season 3 1080p WEB-DL x265-GROUP".into(),
want: NameClaims {
title: s("The Bear"),
resolution: Some(Resolution::P1080),
source: Some(Source::WebDl),
codec: Some(Codec::X265),
group: s("GROUP"),
episode: Some(EpisodeClaim::Season { season: 3 }),
..NameClaims::default()
},
},
Case {
name: "Peep.Show.S01-S09.COMPLETE.1080p.BluRay.x264-SHORTBREHD".into(),
want: NameClaims {
title: s("Peep Show"),
resolution: Some(Resolution::P1080),
source: Some(Source::BluRay),
codec: Some(Codec::X264),
group: s("SHORTBREHD"),
episode: Some(EpisodeClaim::Seasons { first: 1, last: 9 }),
..NameClaims::default()
},
},
Case {
name: "Adventure.Time.S05E01E02.1080p.WEB-DL.AAC2.0.H.264-iT00NZ".into(),
want: NameClaims {
title: s("Adventure Time"),
resolution: Some(Resolution::P1080),
source: Some(Source::WebDl),
codec: Some(Codec::X264),
group: s("iT00NZ"),
episode: episodes(5, &[1, 2]),
..NameClaims::default()
},
},
Case {
name: "Bluey.S03E01-E04.1080p.WEB-DL.DD+5.1.H.264-playWEB".into(),
want: NameClaims {
title: s("Bluey"),
resolution: Some(Resolution::P1080),
source: Some(Source::WebDl),
codec: Some(Codec::X264),
group: s("playWEB"),
episode: episodes(3, &[1, 2, 3, 4]),
..NameClaims::default()
},
},
Case {
name: "The.Daily.Show.2024.05.13.Jon.Stewart.1080p.WEB.h264-EDITH".into(),
want: NameClaims {
title: s("The Daily Show"),
year: Some(2024),
resolution: Some(Resolution::P1080),
source: Some(Source::WebDl),
codec: Some(Codec::X264),
group: s("EDITH"),
episode: Some(EpisodeClaim::Daily {
year: 2024,
month: 5,
day: 13,
}),
..NameClaims::default()
},
},
Case {
name: "Last.Week.Tonight.With.John.Oliver.2024-09-15.720p.HDTV.x264-aAF".into(),
want: NameClaims {
title: s("Last Week Tonight With John Oliver"),
resolution: Some(Resolution::P720),
source: Some(Source::Hdtv),
codec: Some(Codec::X264),
group: s("aAF"),
episode: Some(EpisodeClaim::Daily {
year: 2024,
month: 9,
day: 15,
}),
..NameClaims::default()
},
},
Case {
name: "Dune.Part.Two.2024.2160p.WEB-DL.DDP5.1.Atmos.H.265-FLUX".into(),
want: NameClaims {
title: s("Dune Part Two"),
year: Some(2024),
resolution: Some(Resolution::P2160),
source: Some(Source::WebDl),
codec: Some(Codec::X265),
group: s("FLUX"),
..NameClaims::default()
},
},
@@ -735,6 +897,7 @@ fn all_cases() -> Vec<Case> {
cases.extend(multi_audio_cases());
cases.extend(dv_hdr_cases());
cases.extend(malformed_cases());
cases.extend(tv_cases());
cases
}
+71 -1
View File
@@ -2,7 +2,9 @@
//! corpus pulled from real indexers is issue #9; these rows pin the parser's
//! behaviour per shape of name.
use arr_parse::{parse, Codec, Edition, HdrMarker, LanguageMarker, NameClaims, Resolution, Source};
use arr_parse::{
parse, Codec, Edition, EpisodeClaim, HdrMarker, LanguageMarker, NameClaims, Resolution, Source,
};
use serde as _;
struct Case {
@@ -116,6 +118,10 @@ fn cases() -> Vec<Case> {
source: Some(Source::Hdtv),
codec: Some(Codec::X264),
group: s("LOL"),
episode: Some(EpisodeClaim::Episodes {
season: 1,
episodes: vec![2],
}),
..NameClaims::default()
},
},
@@ -297,6 +303,7 @@ fn cases() -> Vec<Case> {
codec: Some(Codec::X265),
hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr10],
group: s("NTb"),
episode: Some(EpisodeClaim::Season { season: 2 }),
..NameClaims::default()
},
},
@@ -353,3 +360,66 @@ fn garbage_never_panics() {
let _ = parse(input);
}
}
#[test]
fn episode_tags_separate_packs_from_single_episodes() {
let single = parse("Show.S01E02.1080p.WEB-DL.x264-GRP")
.episode
.expect("S01E02 is an episode tag");
assert_eq!(
single,
EpisodeClaim::Episodes {
season: 1,
episodes: vec![2]
}
);
assert!(!single.is_season_pack());
assert!(!single.is_multi_episode());
assert!(single.covers(1, 2));
assert!(!single.covers(1, 3));
assert!(single.covers_season(1));
let multi = parse("Show.S01E02-E04.1080p.WEB-DL.x264-GRP")
.episode
.expect("S01E02-E04 is an episode tag");
assert!(multi.is_multi_episode());
assert!(!multi.is_season_pack());
assert!(multi.covers(1, 3));
assert!(!multi.covers(1, 5));
let pack = parse("Show.S01.1080p.WEB-DL.x264-GRP")
.episode
.expect("S01 is a season tag");
assert!(pack.is_season_pack());
assert!(
pack.covers(1, 7),
"a season pack covers every episode in it"
);
assert!(!pack.covers(2, 7));
let seasons = parse("Show.S01-S03.1080p.WEB-DL.x264-GRP")
.episode
.expect("S01-S03 is a season range");
assert!(seasons.is_season_pack());
assert!(seasons.covers_season(2));
assert!(!seasons.covers_season(4));
let daily = parse("Show.2024.05.13.1080p.WEB.h264-GRP")
.episode
.expect("a dotted air date is a daily tag");
assert!(daily.covers_date(2024, 5, 13));
assert!(!daily.covers_date(2024, 5, 14));
assert!(!daily.covers(1, 1), "a daily episode has no season number");
}
#[test]
fn non_tv_names_claim_no_episode() {
for name in [
"Dune.Part.Two.2024.2160p.WEB-DL.H.265-FLUX",
"Movie.1999.1080p.BluRay.x264-GRP",
"Show.2024.13.45.1080p.WEB-DL.x264-GRP",
"Blade.Runner.2049.2017.2160p.UHD.BluRay.x265-TERMiNAL",
] {
assert_eq!(parse(name).episode, None, "{name}");
}
}