feat(persistence): add .alfred sidecar serializer (DTO ↔ dict)

Step 2 of the specs/dot_alfred.md plan. Pure-dict in/out
(serialize(sidecar) -> dict, deserialize(data) -> ShowSidecar);
YAML I/O lives in the repository layer (step 3) and is kept out
for trivial testability.

DTOs mirror the YAML schema field-for-field:
- ShowSidecar (root: imdb_id, tmdb_id, schema_version, seasons)
- SeasonSidecar (number, path, optional audio/subtitles, optional episodes)
- EpisodeSidecar (number, path, optional audio/subtitles)
- SubtitleEntry (language, source, type)

The sidecar acts as a scan cache: it stores only what is genuinely
costly to recompute — folder/file paths (skipping the FS walk) and
probed track metadata (skipping ffprobe). Release identifiers
(group, source, quality, codec) live in folder/file names and are
derived on demand by the parser; they are deliberately absent from
the schema and rejected as unknown keys on deserialize.

The serializer is strict on schema: unknown keys at any level raise
SidecarSchemaError, missing required fields raise clearly, and bool
cannot sneak in as a season/episode number. Optional fields
(tmdb_id, empty audio/subtitles/episodes) are omitted from the
output rather than emitted as null / [].

Tests cover round-trip equivalence (DTO → dict → DTO and DTO → YAML
text → DTO), the Foundation S01 PACK case (real-world fixture with
mixed sub types — superset captured at season scope), and a
Breaking Bad S05 EPISODIC case. An on-disk tmp_path fixture
recreates the Foundation folder structure with placeholder files,
ready to be reused by the upcoming repository walk tests in step 3.
This commit is contained in:
2026-05-22 16:56:56 +02:00
parent 6c12c18a27
commit b0e275bd11
7 changed files with 862 additions and 0 deletions
+25
View File
@@ -17,6 +17,31 @@ callers).
### Added
- **`.alfred` sidecar serializer
(`alfred/infrastructure/persistence/dot_alfred/`).** Implements step 2
of the `specs/dot_alfred.md` plan. Pure-dict in/out
(`serialize(sidecar) -> dict`, `deserialize(data) -> ShowSidecar`) —
YAML I/O lives in the repository layer (step 3) and is kept out for
trivial testability. Ships the DTOs that mirror the YAML schema
field-for-field (`ShowSidecar`, `SeasonSidecar`, `EpisodeSidecar`,
`SubtitleEntry`). The sidecar acts as a **scan cache**: it stores
only what is genuinely costly to recompute — folder/file paths
(skipping the FS walk) and probed track metadata (skipping ffprobe).
Release identifiers (group, source, quality, codec) live in folder
and file names and are derived on demand by the parser — they are
deliberately absent from the schema and rejected on deserialize. The
serializer is **strict on schema**: unknown keys at any level raise
`SidecarSchemaError`, missing required fields raise clearly, and
`bool` cannot sneak in as a season/episode number. Optional fields
(`tmdb_id`, empty `audio`/`subtitles`/`episodes`) are omitted from
the output rather than emitted as `null` / `[]`. Tests cover
round-trip equivalence (DTO → dict → DTO and DTO → YAML text → DTO),
the Foundation S01 PACK case (real-world fixture with mixed sub
types — superset captured at season scope), and a Breaking Bad S05
EPISODIC case. An on-disk `tmp_path` fixture recreates the Foundation
folder structure with placeholder files, ready to be reused by the
upcoming repository walk tests in step 3.
- **`TVShowBuilder` / `SeasonBuilder` — sole construction surface for the
TVShow aggregate** (`alfred/domain/tv_shows/builders.py`). The aggregate
is now fully frozen; building goes through a mutable scratchpad that