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:
+224
@@ -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**.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Real-world release fixtures — anti-regression baseline for parse_release.
|
||||||
|
|
||||||
|
Each fixture under ``tests/fixtures/releases/<bucket>/<case>/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"
|
||||||
Vendored
+60
@@ -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"
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user