feat(.alfred v2 — Phase 2): Pydantic sidecars, atomic repos, auto-heal index

Spec: specs/dot_alfred_v2.md (Phase 2).

New package alfred/infrastructure/persistence/dot_alfred/v2/:
  * sidecar_release.py / sidecar_root.py — Pydantic DTOs
    (extra="forbid", frozen=True) for per-item sidecars and the
    library-root index. schema_version enforced via model_validator.
  * serializer.py — read_yaml / atomic_write_yaml (.tmp + os.replace).
    SidecarSchemaError wraps YAML + Pydantic errors uniformly.
  * bridge.py — lossless domain <-> sidecar for SeriesRelease /
    MovieRelease; projection-only show_index_entry_from /
    movie_index_entry_from with multi-episode-file flattening.
  * repository.py — DotAlfredSeriesReleaseRepository /
    DotAlfredMovieReleaseRepository (log+skip on corruption),
    DotAlfredTVShowLibraryIndex / DotAlfredMovieLibraryIndex with
    silent auto-heal on missing/corrupt index reads. Writes never
    auto-heal (read paths handle that).

TMDB client extensions:
  * TmdbSeasonInfo / TmdbShowInfo DTOs + pure parse_tv_show_info.
  * TMDBClient.get_tv_show_info aggregates /tv/{id} +
    /tv/{id}/external_ids.

Domain change:
  * SubtitleTrack gains is_sdh: bool = False, populated from
    ffprobe's hearing_impaired disposition. Required for v2 sidecar
    parity (spec replaces v1's type: "sdh" with explicit flag).
    Default keeps every existing caller unchanged.

Tests: 37 new v2 integration tests on tmp_path (round-trips, atomic
writes, schema mismatch handling, anchor warnings, auto-heal paths)
plus 16 TMDB DTO tests. Full suite: 1240 -> 1277 passed.

Implementation notes filed in .claude/specs/dot_alfred_v2_notes.md
(strict=True trade-off, upsert signature deviation from spec, etc.).

Phases 3-5 (TVShow/Movie refactor to TMDB-only, rescan_show rewrite,
v1 deletion + wiring) are next.
This commit is contained in:
2026-05-25 16:01:39 +02:00
parent c0f6d01048
commit e65c1df229
18 changed files with 2565 additions and 3 deletions
+53
View File
@@ -17,6 +17,59 @@ callers).
### Added
- **`.alfred` v2 — Phase 2: new persistence package + TMDB client
extensions.** Second phase of `specs/dot_alfred_v2.md` on branch
`refactor/dot-alfred-v2`. The new
`alfred/infrastructure/persistence/dot_alfred/v2/` package ships
the full v2 sidecar stack while leaving v1 (and the existing
`TVShow` aggregate) untouched — Phase 3 is the cutover.
- **Pydantic DTOs** — `SeriesReleaseSidecar` /
`MovieReleaseSidecar` (per-item), `TVShowLibraryIndexSidecar` /
`MovieLibraryIndexSidecar` (library-root index). All built on a
common `_Strict` base (`extra="forbid"`, `frozen=True`) with a
`@model_validator` enforcing `schema_version == 1`.
- **Track entries** — `AudioTrackEntry` / `SubtitleEntry` (sidecar
cache shape, slimmed from the domain track types). `SubtitleEntry`
carries `is_forced` + `is_sdh` as explicit booleans (v1's
`type: "sdh"` overload is gone).
- **Serializer** — `read_yaml` / `atomic_write_yaml` helpers
centralize YAML I/O and atomic writes (`.tmp + os.replace`).
`SidecarSchemaError` wraps both YAML parse errors and Pydantic
validation errors for uniform catch-and-skip semantics.
- **Bridge** — lossless `domain ↔ sidecar` conversion for
`SeriesRelease` / `MovieRelease` (round-trippable, including
multi-episode ranges and `is_sdh` subtitles); one-way projection
for library-index entries (`show_index_entry_from`,
`movie_index_entry_from`) that flattens multi-episode files into
per-TMDB-slot maps in `seasons[*].episodes`.
- **Repositories** —
`DotAlfredSeriesReleaseRepository` /
`DotAlfredMovieReleaseRepository` walk `library_root/*/` with
log+skip on corruption; **`DotAlfredTVShowLibraryIndex`** /
**`DotAlfredMovieLibraryIndex`** auto-heal silently on missing or
corrupt index files by rebuilding from the per-item sidecars
(healed entries keep TMDB-cached fields as placeholders until the
next sync repopulates them). Writes are atomic and never auto-heal
(read paths handle that).
- **TMDB client extensions** — `TmdbSeasonInfo` / `TmdbShowInfo`
DTOs + `TMDBClient.get_tv_show_info(tmdb_id)` aggregating
`/tv/{id}` + `/tv/{id}/external_ids`. The parsing logic is a pure
function (`parse_tv_show_info`) testable without HTTP, with an
injectable reference date for deterministic `aired` flag tests.
- **`is_sdh` flag on `SubtitleTrack`.** Added to
`alfred/domain/shared/media.py::SubtitleTrack` to mirror ffprobe's
`hearing_impaired` disposition. Wired through the ffprobe layer
(`ffprobe_prober.py`) and the v2 sidecar bridge so SDH information
round-trips end-to-end. Defaults to `False` — backwards-compatible
for every existing caller.
- **37 v2 integration tests** on `tmp_path` covering round-trips
(domain ↔ sidecar ↔ YAML ↔ domain), atomic writes (no `.tmp`
leftovers), per-item log+skip on corruption / schema mismatch,
movie anchor-mismatch warning, full upsert / find / delete on both
library indexes, and the auto-heal path on missing / corrupt /
schema-mismatched index files. **16 TMDB DTO tests** for the new
`parse_tv_show_info` pure function.
- **`.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