test: add real-world release fixtures (EASY bucket)

Captures 5 canonical releases from /mnt/testipool/downloads as parametrized
fixtures under tests/fixtures/releases/easy/. Each fixture declares the
release name, expected ParsedRelease fields, original tree, and the future
routing (library / torrents / seed_hardlinks) for the upcoming organize_media
refactor.

Today only the 'parsed' section is asserted; tree is materialized into a
tmp_path to catch typos. Routing is captured ahead of the planner work — it
becomes verifiable once organize_media lands.

Cases: back_in_action (movie), slow_horses_single_ep (TV single),
foundation_season_pack (S02 + .nfo noise), long_walk_with_noise (movie +
KONTRAST.TOP.txt), sinners_yts (YTS bracket-heavy + Subs/ dir).

Also tracks CHANGELOG.md under [Unreleased] / Added.
This commit is contained in:
2026-05-18 15:36:19 +02:00
parent f17abdbaec
commit 7bc50fd5b8
8 changed files with 568 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
"""Fixture discovery and materialization helpers for release fixtures.
Each fixture is a directory under ``tests/fixtures/releases/<bucket>/<case>/``
containing one ``expected.yaml`` file. See ``releases/README.md`` for the
schema.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import yaml
FIXTURES_ROOT = Path(__file__).parent
@dataclass(frozen=True)
class ReleaseFixture:
"""A loaded fixture, ready to be materialized into a temp dir."""
name: str # "<bucket>/<case>", e.g. "easy/back_in_action"
path: Path # directory containing expected.yaml
data: dict # parsed YAML contents
@property
def release_name(self) -> str:
return self.data["release_name"]
@property
def expected_parsed(self) -> dict:
return self.data.get("parsed", {})
@property
def tree(self) -> list[str]:
return self.data.get("tree", [])
@property
def routing(self) -> dict:
return self.data.get("routing", {})
def materialize(self, root: Path) -> None:
"""Create the fixture's ``tree`` as empty files/dirs under ``root``."""
for entry in self.tree:
target = root / entry
if entry.endswith("/"):
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
target.touch()
def discover_fixtures() -> list[ReleaseFixture]:
"""Find all ``expected.yaml`` files under FIXTURES_ROOT."""
fixtures = []
for yaml_path in sorted(FIXTURES_ROOT.glob("*/*/expected.yaml")):
data = yaml.safe_load(yaml_path.read_text())
name = f"{yaml_path.parent.parent.name}/{yaml_path.parent.name}"
fixtures.append(ReleaseFixture(name=name, path=yaml_path.parent, data=data))
return fixtures
@@ -0,0 +1,30 @@
release_name: "Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST"
parsed:
title: "Back.in.Action"
year: 2025
season: null
episode: null
episode_end: null
quality: "1080p"
source: "WEBRip"
codec: "x265"
group: "KONTRAST"
tech_string: "1080p.WEBRip.x265"
media_type: "movie"
site_tag: null
parse_path: "direct"
is_season_pack: false
tree:
- "Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST/"
- "Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST/Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST.mkv"
routing:
library:
- "Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST/Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST.mkv"
torrents:
- "Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST/"
seed_hardlinks:
- source: "library/Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST/Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST.mkv"
target: "torrents/Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST/Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST.mkv"
@@ -0,0 +1,68 @@
release_name: "Foundation.S02.1080p.x265-ELiTE"
parsed:
title: "Foundation"
year: null
season: 2
episode: null
episode_end: null
quality: "1080p"
source: null
codec: "x265"
group: "ELiTE"
tech_string: "1080p.x265"
media_type: "tv_show"
site_tag: null
parse_path: "direct"
is_season_pack: true
tree:
- "Foundation.S02.1080p.x265-ELiTE/"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02.1080p.x265-ELiTE.nfo"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E01.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E02.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E03.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E04.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E05.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E06.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E07.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E08.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E09.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E10.1080p.x265-ELiTE.mkv"
routing:
library:
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E01.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E02.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E03.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E04.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E05.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E06.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E07.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E08.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E09.1080p.x265-ELiTE.mkv"
- "Foundation.S02.1080p.x265-ELiTE/Foundation.S02E10.1080p.x265-ELiTE.mkv"
torrents:
# Whole original folder moves to torrents/ (carries the .nfo with it)
- "Foundation.S02.1080p.x265-ELiTE/"
seed_hardlinks:
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E01.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E01.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E02.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E02.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E03.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E03.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E04.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E04.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E05.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E05.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E06.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E06.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E07.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E07.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E08.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E08.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E09.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E09.1080p.x265-ELiTE.mkv"
- source: "library/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E10.1080p.x265-ELiTE.mkv"
target: "torrents/Foundation.S02.1080p.x265-ELiTE/Foundation.S02E10.1080p.x265-ELiTE.mkv"
@@ -0,0 +1,32 @@
release_name: "The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST"
parsed:
title: "The.Long.Walk"
year: 2025
season: null
episode: null
episode_end: null
quality: "1080p"
source: "WEBRip"
codec: "x265"
group: "KONTRAST"
tech_string: "1080p.WEBRip.x265"
media_type: "movie"
site_tag: null
parse_path: "direct"
is_season_pack: false
tree:
- "The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/"
- "The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/KONTRAST.TOP.txt"
- "The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST.mkv"
routing:
library:
- "The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST.mkv"
torrents:
# KONTRAST.TOP.txt travels with the folder — never lives in library/
- "The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/"
seed_hardlinks:
- source: "library/The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST.mkv"
target: "torrents/The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST/The.Long.Walk.2025.1080p.WEBRip.x265-KONTRAST.mkv"
+68
View File
@@ -0,0 +1,68 @@
release_name: "Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX]"
# Quirks worth noting (current parse_release behavior):
# - Trailing [YTS.MX] is stripped as a site_tag, leaving a dangling "-" in
# raw/normalised and a UNKNOWN group. We capture this as-is — change requires
# intentional parser work, not silent drift.
parsed:
title: "Sinners"
year: 2025
season: null
episode: null
episode_end: null
quality: "1080p"
source: "WEBRip"
codec: "x265"
group: "UNKNOWN"
tech_string: "1080p.WEBRip.x265"
media_type: "movie"
site_tag: "YTS.MX"
parse_path: "sanitized"
bit_depth: "10bit"
is_season_pack: false
# YTS uses bracket-heavy folder naming and ships a hefty Subs/ dir.
# Selected language defaults from ltm.subtitle_preferences: ["fre", "eng"].
tree:
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].mp4"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/www.YTS.MX.jpg"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/YIFYStatus.com.txt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/ara.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/Brazilian.por.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/English.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/European.fre.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/European.por.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/European.spa.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/ger.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/ita.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/Latin American.spa.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/SDH.eng.HI.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/Traditional.chi.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/tur.srt"
routing:
library:
# Video + sidecar srt + selected eng/fre subs from Subs/
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].mp4"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/English.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/European.fre.srt"
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/SDH.eng.HI.srt"
torrents:
# Whole original folder goes to torrents/ — non-selected subs, jpg, txt all
# travel with it. Anything in library/ gets hard-linked back below.
- "Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/"
seed_hardlinks:
- source: "library/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].mp4"
target: "torrents/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].mp4"
- source: "library/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].srt"
target: "torrents/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX].srt"
- source: "library/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/English.srt"
target: "torrents/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/English.srt"
- source: "library/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/European.fre.srt"
target: "torrents/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/European.fre.srt"
- source: "library/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/SDH.eng.HI.srt"
target: "torrents/Sinners (2025) [1080p] [WEBRip] [x265] [10bit] [5.1] [YTS.MX]/Subs/SDH.eng.HI.srt"
@@ -0,0 +1,32 @@
release_name: "Slow.Horses.S05E01.1080p.WEBRip.x265-KONTRAST"
parsed:
title: "Slow.Horses"
year: null
season: 5
episode: 1
episode_end: null
quality: "1080p"
source: "WEBRip"
codec: "x265"
group: "KONTRAST"
tech_string: "1080p.WEBRip.x265"
media_type: "tv_show"
site_tag: null
parse_path: "direct"
is_season_pack: false
# The download folder happens to be the season pack folder (S05) but only
# one episode is present — typical "single episode trickled in" case.
tree:
- "Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST/"
- "Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST/Slow.Horses.S05E01.1080p.WEBRip.x265-KONTRAST.mkv"
routing:
library:
- "Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST/Slow.Horses.S05E01.1080p.WEBRip.x265-KONTRAST.mkv"
torrents:
- "Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST/"
seed_hardlinks:
- source: "library/Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST/Slow.Horses.S05E01.1080p.WEBRip.x265-KONTRAST.mkv"
target: "torrents/Slow.Horses.S05.1080p.WEBRip.x265-KONTRAST/Slow.Horses.S05E01.1080p.WEBRip.x265-KONTRAST.mkv"