From 7bc50fd5b8e0d1d15838ec75ebdbb125c3e9cf86 Mon Sep 17 00:00:00 2001 From: Francwa Date: Mon, 18 May 2026 15:36:19 +0200 Subject: [PATCH] test: add real-world release fixtures (EASY bucket) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 224 ++++++++++++++++++ tests/domain/test_release_fixtures.py | 54 +++++ tests/fixtures/releases/conftest.py | 60 +++++ .../easy/back_in_action/expected.yaml | 30 +++ .../easy/foundation_season_pack/expected.yaml | 68 ++++++ .../easy/long_walk_with_noise/expected.yaml | 32 +++ .../releases/easy/sinners_yts/expected.yaml | 68 ++++++ .../easy/slow_horses_single_ep/expected.yaml | 32 +++ 8 files changed, 568 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 tests/domain/test_release_fixtures.py create mode 100644 tests/fixtures/releases/conftest.py create mode 100644 tests/fixtures/releases/easy/back_in_action/expected.yaml create mode 100644 tests/fixtures/releases/easy/foundation_season_pack/expected.yaml create mode 100644 tests/fixtures/releases/easy/long_walk_with_noise/expected.yaml create mode 100644 tests/fixtures/releases/easy/sinners_yts/expected.yaml create mode 100644 tests/fixtures/releases/easy/slow_horses_single_ep/expected.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3e85fdf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,224 @@ +# Changelog + +All notable changes to Alfred are documented here. + +The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +Alfred is not yet on SemVer — entries are grouped by **dated work blocks** instead +of release numbers. Granularity targets behavioral or API-visible changes; refer +to `git log` for commit-level detail. + +Sections used per block: **Added** / **Changed** / **Deprecated** / **Removed** / +**Fixed** / **Internal** (for tech-debt and refactor noise that doesn't affect +callers). + +--- + +## [Unreleased] + +### Added + +- **Real-world release fixtures** under `tests/fixtures/releases/{easy,shitty,path_of_pain}/`, + each documenting an expected `ParsedRelease` plus the future `routing` + (library / torrents / seed_hardlinks) for the upcoming `organize_media` + refactor. EASY bucket seeded with 5 cases (movie, single-episode, season + pack, movie + noise, YTS bracket-heavy). Parametrized over + `tests/domain/test_release_fixtures.py` for anti-regression. +- **`NxNN` alt season/episode form supported** by `parse_release`. Releases like + `Show.1x05.720p.HDTV.x264-GRP` and `Show.2x07x08.1080p.WEB.x265-GRP` (multi-ep + alt form) now parse as TV shows. +- **`alfred/knowledge/release/separators.yaml`** declares the token separators + used by the release-name tokenizer (`.`, ` `, `[`, `]`, `(`, `)`, `_`). New + conventions can be added without code changes. The canonical `.` is always + present even if missing from YAML. + +### Changed + +- **`parse_release` tokenizer is now data-driven**: it splits on any character + listed in `separators.yaml` (regex character class) instead of `name.split(".")`. + This makes YTS-style releases (`The Father (2020) [1080p] [WEBRip] [5.1] [YTS.MX]`), + space-separated names (`Inception 2010 1080p BluRay x264-GROUP`), and + underscore-separated names parse correctly via the direct path — no more + fallback through sanitization. +- **`parse_release` flow simplified**: site-tag extraction always runs first + (so `parse_path == "sanitized"` now reliably indicates a stripped `[tag]`), + then well-formedness is checked only against truly forbidden chars + (anything not in the configured separator set). +- **ISO 639-2/B is now the canonical language code project-wide** (was a mix of + 639-1 and 639-2/T): + - `SubtitlePreferences.languages` default is now `["fre", "eng"]` (was + `["fr", "en"]`). Old LTM files are not auto-migrated — delete + `data/memory/ltm.json` to regenerate with the new defaults. + - Subtitle output filenames are now `{iso639_2b}.srt` (e.g. `fre.srt`, + `fre.sdh.srt`). Existing `fr.srt` files are still **read** correctly + (recognized as French via alias) but new files are written canonically. + - `Language` value object docstring corrected: it has always stored 639-2/B + (matching what ffprobe emits), not 639-2/T as previously documented. +- **`MovieService.validate_movie_file` minimum size is now configurable** via + `settings.min_movie_size_bytes` (default unchanged: 100 MB). Constructor + accepts an optional `min_movie_size_bytes` override for tests. +- **`SubtitleKnowledgeBase` delegates language lookup to `LanguageRegistry`** + rather than duplicating tokens. `subtitles.yaml` now only declares + subtitle-specific tokens (e.g. `vostfr`, `vf`, `vff`) under a new + `language_tokens` section. + +### Removed + +- **`alfred/domain/tv_shows/services.py`** and **`alfred/domain/movies/services.py`** + deleted entirely. They held fossil parsers (`parse_episode_filename`, + `extract_movie_metadata`, …) with zero production callers — superseded by + `parse_release` as the single source of truth for release-name parsing. + Associated tests (`tests/domain/test_movies.py`, `tests/domain/test_tv_shows_service.py`) + removed as well. +- `_sanitize` and `_normalize` helpers in `alfred/domain/release/services.py` — + the new tokenizer makes them redundant. +- `_LANG_KEYWORDS`, `_SDH_TOKENS`, `_FORCED_TOKENS`, `SUBTITLE_EXTENSIONS` + hardcoded dicts in `alfred/domain/subtitles/scanner.py` — all knowledge now + lives in YAML (CLAUDE.md compliance). +- `_MIN_MOVIE_SIZE_BYTES` module-level constant in + `alfred/domain/movies/services.py` — replaced by the new setting. +- Top-level `languages:` block in `subtitles.yaml` — superseded by + `language_tokens:` (subtitle-specific only) since iso_languages.yaml is the + canonical source. + +### Fixed + +- **`hi` token no longer marks a subtitle as SDH** (it conflicted with the + ISO 639-1 alias for Hindi). SDH is now detected only via `sdh`, `cc`, and + `hearing` tokens. +- `SubtitleKnowledgeBase` default rules used `"fra"` while + `iso_languages.yaml` exposes French as `"fre"` — preferred languages + defaults now match the canonical form. + +### Internal + +- Removed backward-compat shims `_sanitise_for_fs` / + `_strip_episode_from_normalised` from `domain/release/value_objects.py` + (zero callers). +- Cleaned ruff warnings across the codebase: `subprocess.run` calls now pass + explicit `check=False` (PLW1510); lazy imports promoted to module top where + there was no cycle (PLC0415 in `manage_subtitles.py`, `placer.py`, + `qbittorrent/client.py`, `file_manager.py`); fixed module-level import + ordering (E402) in `language_registry.py` and `subtitles/knowledge/loader.py`; + removed unused locals (F841 / B007); replaced unnecessary set comprehension + with `set()` in `release/knowledge.py` (C416). +- Ruff config: ignore `PLR0911` / `PLR0912` (too-many-returns / too-many-branches) + globally — noisy on parser mappers and orchestrator use-cases where early-return + validation is essential complexity. Ignore `PLW0603` for the documented memory + singleton (`infrastructure/persistence/context.py`). + +--- + +## [2026-05-17] — TVShow & Movie aggregate refactor + +Multi-phase refonte of the TV show domain into a real DDD aggregate, with +matching parity work on `Movie`, a language knowledge system, and the +`shared/media` restructure that supports both. + +### Added + +- **Language knowledge system** (`alfred/knowledge/iso_languages.yaml` + 42 + languages including `und` for undetermined). + - `Language` value object (frozen dataclass) with `iso`, `english_name`, + `native_name`, `aliases`, and a `matches(raw)` cross-format helper. + - `LanguageRegistry` loader (`alfred/domain/shared/knowledge/`) merging + builtin + learned YAML. Not a singleton — the application layer + instantiates it. + - ISO 639-2/B is the canonical key; aliases cover 639-1, 639-2/T, English + name, native name, and common spellings. +- **`VideoTrack`** dataclass (`alfred/domain/shared/media/video.py`) with a + `resolution` property using width-priority bucket detection (handles + cinema/scope crops like 1920×960 → 1080p). +- **`shared/media/matching.py`** — `track_lang_matches` helper shared by + `Episode` and `Movie`. Implements the **"C+" contract** for language helpers: + - `Language` query → cross-format match via `Language.matches()` + - `str` query → case-insensitive direct comparison (no normalization) +- **TVShow aggregate composition**: + - `TVShow.seasons: dict[SeasonNumber, Season]` + - `Season.episodes: dict[EpisodeNumber, Episode]` + - `Season.expected_episodes` / `Season.aired_episodes` (split so collection + state can compare "owned vs aired today" without confusing in-flight + seasons with future ones) +- **Aggregate methods on `TVShow`**: + - `add_episode(ep)` — sole sanctioned mutation entry point (creates the + season if missing) + - `add_season(season)` — replaces a season wholesale + - `collection_status()` → `CollectionStatus.{EMPTY, PARTIAL, COMPLETE}` + - `is_complete_series()` — true iff `ENDED + COMPLETE` + - `missing_episodes()` — flat list of all aired-but-not-owned + `(season, episode)` pairs +- **`CollectionStatus`** enum (orthogonal to `ShowStatus`). +- **Episode track helpers** (`has_audio_in`, `has_subtitles_in`, + `has_forced_subs`, `audio_languages`, `subtitle_languages`), driven by + `Episode.audio_tracks` / `Episode.subtitle_tracks`. +- **Movie aggregate parity** — `Movie` now carries `audio_tracks` / + `subtitle_tracks` and exposes the same helpers as `Episode` (same C+ + contract). +- **`CHANGELOG.md`** (this file). + +### Changed + +- **`shared/media_info.py` exploded into `shared/media/{audio,video,subtitle,info,matching}.py`.** + `MediaInfo` is now symmetric: every stream type is a `list[Track]`. Flat + accessors (`width`, `height`, `video_codec`, `resolution`) remain as + properties that read the first video track. +- **`MediaInfo.duration_seconds` / `bitrate_kbps`** moved from `VideoTrack` to + `MediaInfo` (file-level — they come from the ffprobe `format` block, not a + stream). Files without a video stream now correctly expose duration. +- **`ShowStatus.from_string`** extended to map TMDB strings (`Returning + Series`, `In Production`, `Pilot`, `Planned`, `Canceled`, `Cancelled`). + Comparison is whitespace-trimmed and case-insensitive. +- **`Season` / `Episode`** dropped their `show_imdb_id` back-references. They + are owned by `TVShow` and reached only through it. +- **`TVShow.seasons_count` and `episode_count`** are now `@property` (computed + from the dict) instead of stored ints. +- **`TVShowService.parse_episode_from_filename`** rewritten in string + operations (no regex). Supports `S01E05` / `s1e5` and `1x05` / `01x5` forms. +- **`TVShowService.find_next_episode`** now drives off + `show.missing_episodes()` instead of the hardcoded "max 50 episodes per + season" heuristic. +- **`TVShowService` constructor** no longer takes `season_repository` / + `episode_repository` — the aggregate persists in one block via + `TVShowRepository` only. +- **`SubtitleTrack` in `alfred.domain.subtitles.entities` renamed to + `SubtitleCandidate`.** Coexists with the `shared.media.SubtitleTrack` + ffprobe-view dataclass (different bounded contexts, kept separate + intentionally). +- **`tv_shows/services.py` `_VIDEO_EXTENSIONS`** now loaded from + `knowledge/release/file_extensions.yaml` via `load_video_extensions()` + (single source of truth). +- **`CLAUDE.md`** updated with three new policy sections: + - "Tests" — small updates OK during normal work, no mass-update sprees + - "Backwards-compatibility shims" — prefer clean migration over shims + - "Regex" — not forbidden, use judgment when string ops would be fragile + +### Removed + +- **Legacy `Season N Episode N` filename form** in + `TVShowService.parse_episode_from_filename`. It never appears in the release + names Alfred handles, and supporting it forced a regex. +- **`SeasonRepository` and `EpisodeRepository`** — only the aggregate root has + a repository (DDD rule: one repo per aggregate). +- **`shared/media_info.py`** compatibility shim — callers updated. +- **`SubtitleTrack` compatibility alias** in `subtitles.entities` — callers + updated to `SubtitleCandidate`. + +### Fixed + +- **`MediaInfo.duration_seconds` returns `None` on audio-only files** instead + of crashing through `primary_video.duration_seconds` (see the duration/bitrate + move under **Changed**). +- **`MediaOrganizer`** (`infrastructure/filesystem/organizer.py`) no longer + passes the removed `show_imdb_id` / `episode_count` kwargs when constructing + a `Season` for folder-name generation. + +### Internal + +- Test suite rewritten where the aggregate redesign broke fixtures: + `tests/domain/test_tv_shows.py` (69 tests), `tests/domain/test_media_info.py` + (rewritten for `VideoTrack`), `tests/application/test_enrich_from_probe.py` + (helper added), `tests/infrastructure/test_filesystem_extras.py` (fixtures), + `tests/domain/test_tv_shows_service.py` (find_next_episode driven by real + aggregate state). +- Subtitle services internal migration: `matcher.py`, `utils.py`, `placer.py`, + `identifier.py` updated to import `SubtitleCandidate`. +- Suite status at end of block: **1066 passed, 8 skipped, 0 failed**. diff --git a/tests/domain/test_release_fixtures.py b/tests/domain/test_release_fixtures.py new file mode 100644 index 0000000..1ea42dc --- /dev/null +++ b/tests/domain/test_release_fixtures.py @@ -0,0 +1,54 @@ +"""Real-world release fixtures — anti-regression baseline for parse_release. + +Each fixture under ``tests/fixtures/releases///expected.yaml`` +declares a release name and the ``ParsedRelease`` fields it should produce. +Fields absent from the fixture's ``parsed`` block are not checked, so adding +new attributes to ``ParsedRelease`` never breaks existing fixtures. + +The fixture's ``tree`` is materialized into a temp dir to prove the layout is +self-consistent, even though no filesystem assertions are made yet. The +``routing`` block (library / torrents / seed_hardlinks) is captured ahead of +the ``organize_media`` refactor — it will become verifiable once the planner +exists. +""" + +from __future__ import annotations + +from dataclasses import asdict + +import pytest + +from alfred.domain.release.services import parse_release +from tests.fixtures.releases.conftest import ReleaseFixture, discover_fixtures + +FIXTURES = discover_fixtures() + + +@pytest.mark.parametrize( + "fixture", + FIXTURES, + ids=[f.name for f in FIXTURES], +) +def test_parse_matches_fixture(fixture: ReleaseFixture, tmp_path) -> None: + # Materialize the tree to assert it is at least well-formed YAML + + # plausible filesystem paths. Catches typos / missing leading dirs early. + fixture.materialize(tmp_path) + + result = asdict(parse_release(fixture.release_name)) + # ``is_season_pack`` is a @property — asdict() does not include it. + result["is_season_pack"] = parse_release(fixture.release_name).is_season_pack + + for field, expected in fixture.expected_parsed.items(): + assert field in result, ( + f"{fixture.name}: unknown field '{field}' in expected.parsed" + ) + assert result[field] == expected, ( + f"{fixture.name}: parsed.{field} — " + f"expected {expected!r}, got {result[field]!r}" + ) + + +def test_at_least_one_fixture_per_bucket() -> None: + """Each bucket should hold at least one case once populated.""" + buckets = {f.name.split("/")[0] for f in FIXTURES} + assert "easy" in buckets, "EASY bucket must have at least one fixture" diff --git a/tests/fixtures/releases/conftest.py b/tests/fixtures/releases/conftest.py new file mode 100644 index 0000000..265b0c0 --- /dev/null +++ b/tests/fixtures/releases/conftest.py @@ -0,0 +1,60 @@ +"""Fixture discovery and materialization helpers for release fixtures. + +Each fixture is a directory under ``tests/fixtures/releases///`` +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 # "/", 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 diff --git a/tests/fixtures/releases/easy/back_in_action/expected.yaml b/tests/fixtures/releases/easy/back_in_action/expected.yaml new file mode 100644 index 0000000..dcbed1c --- /dev/null +++ b/tests/fixtures/releases/easy/back_in_action/expected.yaml @@ -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" diff --git a/tests/fixtures/releases/easy/foundation_season_pack/expected.yaml b/tests/fixtures/releases/easy/foundation_season_pack/expected.yaml new file mode 100644 index 0000000..f169ef3 --- /dev/null +++ b/tests/fixtures/releases/easy/foundation_season_pack/expected.yaml @@ -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" diff --git a/tests/fixtures/releases/easy/long_walk_with_noise/expected.yaml b/tests/fixtures/releases/easy/long_walk_with_noise/expected.yaml new file mode 100644 index 0000000..2b10d14 --- /dev/null +++ b/tests/fixtures/releases/easy/long_walk_with_noise/expected.yaml @@ -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" diff --git a/tests/fixtures/releases/easy/sinners_yts/expected.yaml b/tests/fixtures/releases/easy/sinners_yts/expected.yaml new file mode 100644 index 0000000..9729b8a --- /dev/null +++ b/tests/fixtures/releases/easy/sinners_yts/expected.yaml @@ -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" diff --git a/tests/fixtures/releases/easy/slow_horses_single_ep/expected.yaml b/tests/fixtures/releases/easy/slow_horses_single_ep/expected.yaml new file mode 100644 index 0000000..3bec0c7 --- /dev/null +++ b/tests/fixtures/releases/easy/slow_horses_single_ep/expected.yaml @@ -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"