mod tokens; use regex::Regex; use std::sync::LazyLock; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Source { Hdtv, WebRip, WebDl, BluRay, Remux, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Codec { H264, Hevc, Av1, } #[derive(Debug, Clone, PartialEq, Default)] pub struct ParsedRelease { pub title_raw: String, pub title_normalized: String, pub group: Option, pub season: Option, pub episode: Option, pub absolute_episode: Option, pub year: Option, pub resolution: Option, pub source: Option, pub codec: Option, pub bit_depth: Option, pub container: Option, pub is_repack: bool, } pub fn parse(raw_title: &str) -> ParsedRelease { let mut work = raw_title.to_string(); let group = tokens::extract_group(&work); if group.is_some() { work = tokens::GROUP_PREFIX_RE .replace(&work, "") .trim() .to_string(); } let container = tokens::extract_container(&work); let resolution = tokens::extract_resolution(&work); let source = tokens::extract_source(&work); let codec = tokens::extract_codec(&work); let bit_depth = tokens::extract_bit_depth(&work); let is_repack = tokens::REPACK_RE.is_match(&work); let year = tokens::extract_year(&work); let (season, episode, absolute_episode, title_span_end) = tokens::extract_episode_info(&work); let title_normalized = tokens::derive_title(&work, title_span_end); ParsedRelease { title_raw: raw_title.to_string(), title_normalized, group, season, episode, absolute_episode, year, resolution, source, codec, bit_depth, container, is_repack, } } pub(crate) static WS_RE: LazyLock = LazyLock::new(|| Regex::new(r"\s+").unwrap()); #[cfg(test)] mod tests { use super::*; const CORPUS: &[&str] = &[ "Aime ton prochain (01-06) (Chida) (2019) [Digital-1920] [Manga FR] (PapriKa+)", "[ANi] 小書痴的下剋上 為了成為圖書管理員不擇手段!領主的養女 - 13 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]", "Ascendance of a Bookworm S04E11 The Gathering of Gutenberg 1080p CR WEB-DL DUAL AAC2.0 H 264-VARYG (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Dual-Audio, Multi-Subs)", "Ascendance of a Bookworm S04E12 The Winter Social Season and Debut 1080p CR WEB-DL DUAL AAC2.0 H 264-VARYG (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Dual-Audio, Multi-Subs)", "Ascendance of a Bookworm S04E13 VOSTFR 1080p WEB x264 AAC -Tsundere-Raws (CR) (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen - Ryoushu no Youjo,Ascendance of a Bookworm: Adopted Daughter of an Archduke)", "Ascendance of a Bookworm S04E13 VOSTFR 720p WEB x264 AAC -Tsundere-Raws (CR) (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen - Ryoushu no Youjo,Ascendance of a Bookworm: Adopted Daughter of an Archduke)", "Ascendance of a Bookworm S04E13 Winter Material Gathering 1080p CR WEB-DL AAC2.0 H 264-VARYG (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Multi-Subs)", "Assassin's Creed - Blade of Shao Jun (01-04) (Kurata) (2020) [Digital-1920] [Manga FR] (PapriKa+)", "[Erai-raws] Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen - Ryushu no Youjo - 13 [1080p CR WEB-DL AVC AAC][MultiSub][18261A02]", "[Erai-raws] Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen - Ryushu no Youjo - 13 [480p CR WEB-DL AVC AAC][MultiSub][A22FE68E]", "[Erai-raws] Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen - Ryushu no Youjo - 13 [720p CR WEB-DL AVC AAC][MultiSub][4D409859]", "[GM-Team][国漫][仙逆][Renegade Immortal][2023][148][AVC][GB][1080P]", "[GM-Team][国漫][光阴之外][Beyond Time's Gaze][2025][29][GB][4K HEVC 10Bit]", "[GM-Team][国漫][吞噬星空][Swallowed Star][2021][231][AVC][GB][1080P]", "[GM-Team][国漫][斗破苍穹 第5季][Fights Break Sphere Ⅴ][2022][206][AVC][GB][1080P]", "[GM-Team][国漫][斗破苍穹 第5季][Fights Break Sphere Ⅴ][2022][206][HEVC][GB][4K]", "[GM-Team][国漫][牧神记][Tales of Qin Mu][2024][90][AVC][GB][1080P]", "KILL BLUE S01E12 Life Paths 1080p AMZN WEB-DL MULTi DDP2.0 H 264-VARYG (Kill Ao, Multi-Audio, Multi-Subs)", "[LoliHouse] 与奔驰于透明之夜的你,谈一场看不见的恋爱。 / 透明な夜に駆ける君と、目に見えない恋をした。 / KakeKoi - 01 [WebRip 1080p HEVC-10bit AAC][简繁内封字幕]", "RILAKKUMA S01E13 1080p CR WEB-DL MULTi AAC2.0 H 264-VARYG (Multi-Audio, Multi-Subs)", "[RUBaDUB] Kaiju No. 8 (S1 Complete) (1080p) (Dual Audio)", "[SubsPlease] Honzuki no Gekokujou S4 - 13 (1080p) [A4FE0990].mkv", "[SubsPlease] Honzuki no Gekokujou S4 - 13 (480p) [17152F60].mkv", "[SubsPlease] Honzuki no Gekokujou S4 - 13 (720p) [97744C2D].mkv", "That Time I Got Reincarnated as a Slime S04E12 Tempest Evolves 1080p CR WEB-DL DUAL AAC2.0 H 264-VARYG (Tensei shitara Slime Datta Ken, Dual-Audio, Multi-Subs)", "The Drops of God S01E12 Take the Distant Path 1080p CR WEB-DL DUAL AAC2.0 H 264-VARYG (Kami no Shizuku, Dual-Audio, Multi-Subs)", "[ToonsHub] Ascendance of a Bookworm S04E11 1080p CR WEB-DL DUAL AAC2.0 H.264 (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Dual-Audio, Multi-Subs)", "[ToonsHub] Ascendance of a Bookworm S04E12 1080p CR WEB-DL DUAL AAC2.0 H.264 (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Dual-Audio, Multi-Subs)", "[ToonsHub] Ascendance of a Bookworm S04E13 1080p CR WEB-DL AAC2.0 H.264 (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Multi-Subs)", "[ToonsHub] Honzuki no Gekokujou S04E13 1080p AMZN WEB-DL DDP2.0 H.264 (Ascendance of a Bookworm Side Story)", "[Yameii] Ascendance of a Bookworm - S04E11 [English Dub] [CR WEB-DL 1080p H264 AAC] [8ACE7B72] (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan o Erande Iraremasen - Ryoushu no Youjo | Adopted Daughter of an Archduke)", "[Yameii] Ascendance of a Bookworm - S04E12 [English Dub] [CR WEB-DL 1080p H264 AAC] [ACE19921] (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan o Erande Iraremasen - Ryoushu no Youjo | Adopted Daughter of an Archduke)", "[桜都字幕组] 入间同学入魔了 第四季 / Mairimashita! Iruma-kun (2026) [13][1080P][简体内嵌]", "[桜都字幕组] 入间同学入魔了 第四季 / Mairimashita! Iruma-kun (2026) [13][1080P][简繁内封]", "[桜都字幕组] 入间同学入魔了 第四季 / Mairimashita! Iruma-kun (2026) [13][1080P][繁体内嵌]", "[桜都字幕组] 入间同学入魔了 第四季 / Mairimashita! Iruma-kun (2026) [14][1080P][简繁内封]", ]; #[test] fn parses_standard_sxxexx_with_group_and_quality_chain() { let p = parse("Ascendance of a Bookworm S04E11 The Gathering of Gutenberg 1080p CR WEB-DL DUAL AAC2.0 H 264-VARYG (Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen, Dual-Audio, Multi-Subs)"); assert_eq!(p.season, Some(4)); assert_eq!(p.episode, Some(11)); assert_eq!(p.resolution, Some(1080)); assert_eq!(p.source, Some(Source::WebDl)); assert_eq!(p.codec, Some(Codec::H264)); assert_eq!(p.title_normalized, "Ascendance of a Bookworm"); } #[test] fn parses_sxxexx_with_a_trailing_fansub_revision_tag() { // "v2" glued directly onto the episode number ("a fixed re-release // of this episode") previously broke the word-boundary check // entirely, leaving season/episode both None. let p = parse("[Judas] Chainsaw Man - S01E01v2 1080p WEB-DL"); assert_eq!(p.season, Some(1)); assert_eq!(p.episode, Some(1)); } #[test] fn parses_subsplease_dash_episode_with_group_and_hash() { let p = parse("[SubsPlease] Honzuki no Gekokujou S4 - 13 (1080p) [A4FE0990].mkv"); assert_eq!(p.group.as_deref(), Some("SubsPlease")); assert_eq!(p.season, Some(4)); assert_eq!(p.episode, Some(13)); assert_eq!(p.resolution, Some(1080)); assert_eq!(p.container.as_deref(), Some("mkv")); assert_eq!(p.title_normalized, "Honzuki no Gekokujou"); } #[test] fn parses_erai_raws_bracket_quality_block() { let p = parse("[Erai-raws] Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen - Ryushu no Youjo - 13 [1080p CR WEB-DL AVC AAC][MultiSub][18261A02]"); assert_eq!(p.group.as_deref(), Some("Erai-raws")); assert_eq!(p.season, None); assert_eq!(p.episode, Some(13)); assert_eq!(p.resolution, Some(1080)); assert_eq!(p.source, Some(Source::WebDl)); } #[test] fn parses_lolihouse_webrip_hevc_10bit() { let p = parse("[LoliHouse] Some Title - 01 [WebRip 1080p HEVC-10bit AAC][Subs]"); assert_eq!(p.group.as_deref(), Some("LoliHouse")); assert_eq!(p.episode, Some(1)); assert_eq!(p.source, Some(Source::WebRip)); assert_eq!(p.codec, Some(Codec::Hevc)); assert_eq!(p.bit_depth, Some(10)); } #[test] fn parses_season_pack_no_episode() { let p = parse("[RUBaDUB] Kaiju No. 8 (S1 Complete) (1080p) (Dual Audio)"); assert_eq!(p.group.as_deref(), Some("RUBaDUB")); assert_eq!(p.season, Some(1)); assert_eq!(p.episode, None); assert_eq!(p.resolution, Some(1080)); } #[test] fn parses_season_pack_shapes_without_literal_parens() { // Real review-queue entries that all failed to resolve at all // before this fix — season came back None, not just unmatched // episode — because SEASON_PACK_RE required a literal // "(S?N Complete)" shape. assert_eq!( parse("Modern.Family.S10.COMPLETE.720p.AMZN.WEBRip.x264-GalaxyTV").season, Some(10) ); assert_eq!( parse("Modern Family 2009 Season 8 Complete 720p AMZN WEBRip x264 [i_c]").season, Some(8) ); assert_eq!( parse("Red.Dwarf.S04.1080p.BluRay.x264-LATENCY [Season 4 Four Complete]").season, Some(4) ); assert_eq!( parse("Red.Dwarf.S11.1080p.BluRay.x264-SHORTBREHD [Season 11 Eleven]").season, Some(11) ); assert_eq!( parse("Game of Thrones - Season 8 S08 - 2019 1080p Bluray AAC5.1 x264-R").season, Some(8) ); } // Regression test for a real gap found in review: "Season 2 - 25" was // first swallowed whole by `looks_like_episode_range` (its own // `BARE_EPISODE_RANGE_RE` skips over the un-matchable "Season" word and // finds its first real match at "2 - 25", mistaking the season marker's // own number for a range start), and even with that fixed, // `extract_episode_info`'s `SEASON_PACK_RE` branch used to return // season-only and never look for a trailing episode number at all. // Either bug alone drops episode 25 silently. #[test] fn parses_a_season_marker_followed_by_a_dash_episode() { let p = parse("[Erai-raws] Some Show Season 2 - 25 [1080p]"); assert_eq!(p.season, Some(2)); assert_eq!(p.episode, Some(25)); } // Companion case: a genuine season-only pack (no trailing dash-episode // anywhere) must still resolve to season-only, not spuriously pick up // an unrelated number as an episode. #[test] fn a_genuine_season_only_pack_with_no_dash_episode_still_has_no_episode() { let p = parse("Some Show Season 2 Complete [1080p]"); assert_eq!(p.season, Some(2)); assert_eq!(p.episode, None); } // Regression test for a real gap found in review: a date-named release // ("2024-01-15") got misread by `BARE_EPISODE_RANGE_RE` as an episode // range — the 4-digit year is too many digits for `\d{1,3}` to match // whole, so its first real match starts at the month/day pair // ("01-15") instead, and `looks_like_episode_range` treats that as a // real range. Asserted directly against the tokenizer rather than // `parse()`, since a false-positive range and a genuine "no episode // marker at all" both surface identically as `None`/`None` on // `ParsedRelease` — `looks_like_episode_range` returning `false` is the // actual fix being tested here. #[test] fn does_not_mistake_a_yyyy_mm_dd_date_for_an_episode_range() { assert!(!tokens::looks_like_episode_range("Some Daily Show 2024-01-15 1080p WEB-DL")); let p = parse("Some Daily Show 2024-01-15 1080p WEB-DL"); assert_eq!(p.season, None); assert_eq!(p.episode, None); } #[test] fn parses_yameii_dash_sxxexx_with_english_dub_tag() { let p = parse("[Yameii] Ascendance of a Bookworm - S04E11 [English Dub] [CR WEB-DL 1080p H264 AAC] [8ACE7B72] (Honzuki no Gekokujou)"); assert_eq!(p.group.as_deref(), Some("Yameii")); assert_eq!(p.season, Some(4)); assert_eq!(p.episode, Some(11)); assert_eq!(p.codec, Some(Codec::H264)); } #[test] fn parses_year_and_bare_episode_number() { let p = parse("[GM-Team][Renegade Immortal][2023][148][AVC][GB][1080P]"); assert_eq!(p.year, Some(2023)); assert_eq!(p.resolution, Some(1080)); assert_eq!(p.codec, Some(Codec::H264).or(Some(Codec::H264))); // AVC == H264 family } /// A batch of real, unmodified nyaa.si release titles pulled live /// across several categories — not hand-picked for parseability. Not a /// correctness check (no ground truth for the messier CJK/bracket-only /// shapes), just a robustness net: parse() must never panic, and on /// the common WEB-DL/anime shapes it should extract *something*. #[test] fn does_not_panic_on_real_corpus() { let mut resolution_hits = 0; for title in CORPUS { let p = parse(title); if p.resolution.is_some() { resolution_hits += 1; } assert!(!p.title_normalized.is_empty(), "empty title for {title:?}"); } // Most of this corpus carries an explicit resolution tag; a low hit // rate would mean the resolution regex regressed, not just that a // few CJK/manga edge cases were missed. assert!( resolution_hits * 2 > CORPUS.len(), "only {resolution_hits}/{} titles yielded a resolution", CORPUS.len() ); } #[test] fn refuses_to_resolve_a_bracketed_batch_range_to_one_episode() { // Previously resolved to episode 12, silently attributing the // whole 12-episode batch torrent to one guessed episode. let p = parse("[Judas] Some Show (01-12) [BD 1080p]"); assert_eq!(p.episode, None); assert_eq!(p.absolute_episode, None); } #[test] fn refuses_to_resolve_a_dash_prefixed_batch_range_to_one_episode() { // Previously resolved to episode 1 (the first number after the // leading dash), same underlying problem as above. let p = parse("Show - 01-12 [1080p]"); assert_eq!(p.episode, None); assert_eq!(p.absolute_episode, None); } #[test] fn refuses_to_resolve_an_sxxexx_range_to_one_episode() { // Previously resolved to S01E01 via SXXEXX_RE matching only the // first half of the range. let p = parse("Show S01E01-E12 1080p WEB-DL"); assert_eq!(p.season, Some(1)); assert_eq!(p.episode, None); assert_eq!(p.absolute_episode, None); } #[test] fn single_episode_dash_titles_are_unaffected_by_range_detection() { // A genuine single episode using dash notation must still resolve // normally — only real ranges should be refused. let p = parse("[SubsPlease] Some Show - 05 (1080p) [ABCD1234].mkv"); assert_eq!(p.episode, Some(5)); assert_eq!(p.absolute_episode, Some(5)); } #[test] fn extracts_a_bare_year_from_a_scene_style_movie_name() { // Previously parsed as year=None entirely, since these releases // never bracket the year — silently disabling movie_year_mismatch, // the only defense against a same-named-but-wrong film, on exactly // the naming convention TPB/1337x results actually use. let p = parse("Dune.1984.1080p.BluRay.x264-GROUP"); assert_eq!(p.year, Some(1984)); let p = parse("Dune 1984 1080p BluRay"); assert_eq!(p.year, Some(1984)); } #[test] fn does_not_mistake_a_year_titled_movie_for_a_bare_year() { // "1917" and "2012" are real movie titles — a bare year at the very // start of the string must not be treated as a release year, or // the title would be mistaken for empty. let p = parse("1917 1080p BluRay x264-GROUP"); assert_eq!(p.year, None); } #[test] fn brackets_still_take_priority_over_a_coincidental_bare_year() { let p = parse("Some.Show.2024.S01E01.(2023).1080p.WEB-DL"); assert_eq!(p.year, Some(2023)); } #[test] fn does_not_panic_on_unparsable_manga_release() { // Not a video release at all — should degrade gracefully, not crash. let p = parse("Aime ton prochain (01-06) (Chida) (2019) [Digital-1920] [Manga FR] (PapriKa+)"); assert_eq!(p.year, Some(2019)); // season/episode extraction is allowed to miss here; the important // thing is it returns *something* rather than panicking. let _ = p.season; } }