feat(releases): Phase 1 — new filesystem release domain + TmdbId VO

First step of specs/dot_alfred_v2.md. Introduces a separate bounded
context (alfred/domain/releases/) for the filesystem-side aggregates,
disjoint from TMDB identity which stays in tv_shows/ and movies/.
The link between the two worlds is TmdbId, used as the natural key
in the persistence layer (no domain-level reference).

New package alfred/domain/releases/:
- value_objects: EpisodeRange (covers SxxE01E02E03 multi-episode
  files via start/end inclusive range, with count/numbers/is_single
  helpers), ReleaseMode enum (PACK = N video files direct in the
  season folder, EPISODIC = N sub-folders).
- entities: TrackProfile, EpisodeRelease, SeasonRelease (with
  episode_count() summing each EpisodeRange.count()), SeriesRelease
  (tmdb_id primary anchor, optional imdb_id secondary), MovieRelease.
  All frozen dataclasses.
- builders: SeasonReleaseBuilder + SeriesReleaseBuilder mirroring
  the v1 TVShowBuilder pattern. Builders sort episodes by range
  start on emit and reject overlapping ranges (two files claiming
  the same TMDB slot). from_existing() seeds a builder from an
  existing frozen aggregate for round-trip edits.
- repositories: abstract ports (SeriesReleaseRepository,
  MovieReleaseRepository); concrete .alfred sidecar impls arrive
  in Phase 2.

New shared VO alfred/domain/shared/value_objects.py::TmdbId — positive
int, rejects bool/str/float, symmetric with the existing ImdbId VO.

73 unit tests cover VO validation, entity invariants, builder sort
+ overlap detection, and from_existing() round-trips.

v1 code paths are untouched at this stage; the new domain coexists
with the old TVShow aggregate until Phase 3 refactors it.
This commit is contained in:
2026-05-25 15:19:23 +02:00
parent de7030fa9c
commit c0f6d01048
12 changed files with 1316 additions and 2 deletions
+27
View File
@@ -17,6 +17,33 @@ callers).
### Added
- **`.alfred` v2 — Phase 1: new `releases/` domain.** First step of
`specs/dot_alfred_v2.md` on branch `refactor/dot-alfred-v2`. The
new `alfred/domain/releases/` package introduces a filesystem-only
bounded context separated from TMDB identity (the existing
`tv_shows` / `movies` domains). It hosts:
- **`EpisodeRange` VO** — covers single-episode files
(`EpisodeRange(E02, E02)`) and multi-episode files
(`EpisodeRange(E02, E04)` for `SxxE02E03E04.mkv`), with
`count()` / `numbers()` / `is_single()` helpers.
- **`ReleaseMode` enum** — `PACK` (N video files directly in the
season folder) vs `EPISODIC` (N sub-folders, one episode each);
classified by the walker, never re-derived.
- **Aggregates** — `TrackProfile`, `EpisodeRelease`,
`SeasonRelease` (with `episode_count()` summing each file's
range), `SeriesRelease`, `MovieRelease`. All frozen
dataclasses; mutation via `SeasonReleaseBuilder` /
`SeriesReleaseBuilder` (mirror the v1 `TVShowBuilder` pattern,
including `from_existing()` round-trip).
- **Abstract ports** — `SeriesReleaseRepository`,
`MovieReleaseRepository` (concrete `DotAlfred*` arrive in
Phase 2).
- **`TmdbId` VO** added to `alfred/domain/shared/value_objects.py`
(positive int, rejects bool/str/float — symmetry with `ImdbId`).
- 73 unit tests covering VO validation, entity invariants, builder
sort + overlap detection, and `from_existing()` round-trips. v1
code paths untouched at this stage; new domain coexists.
- **`rescan_show` orchestrator
(`alfred/application/library/rescan.py`).** Step 4 of the
`specs/dot_alfred.md` plan. Walks an Alfred-managed show folder,