feat(persistence): add DotAlfredTVShowRepository (filesystem-backed)

Step 3 of specs/dot_alfred.md. Concrete TVShowRepository
implementation reading and writing per-show .alfred YAML files under
a configurable library_root. Writes are atomic (.alfred.tmp +
os.replace), reads tolerate corrupted/wrong-schema sidecars (log +
skip), and the repo never invents a folder name — save(show)
requires the target folder to exist beforehand (raises
ShowFolderUnknown otherwise), matching the spec's
MediaOrganizer-then-sidecar split.

Cold folders without a sidecar are skipped by find_all and yield
None from find_by_imdb_id — the upcoming rescan_show tool (step 4)
will own the opt-in rebuild path.

A small bridge module translates between the rich domain TVShow
(AudioTrack/SubtitleTrack with full ffprobe minutiae) and the
compact sidecar shape (language-only audio, embedded-only subs with
type derived from is_forced). The bridge is intentionally lossy on
probe details the sidecar does not store, per the spec's
factual-only philosophy.

20 integration tests on tmp_path: round-trip save/find,
cold-folder/unknown-id returns, find_all skipping
(corrupted/schema-violating sidecars), delete/exists, atomic write
(no .alfred.tmp leftover), overwrite, and folder-name fallbacks
(get_folder_name guess + full-scan rescue when renamed).
This commit is contained in:
2026-05-22 17:16:41 +02:00
parent b0e275bd11
commit c7c11180d9
5 changed files with 721 additions and 0 deletions
+35
View File
@@ -17,6 +17,41 @@ callers).
### Added
- **`DotAlfredTVShowRepository` — filesystem-backed implementation of
the `TVShowRepository` port
(`alfred/infrastructure/persistence/dot_alfred/repository.py`).**
Step 3 of the `specs/dot_alfred.md` plan. Reads and writes one
`.alfred` YAML file per show under a configurable `library_root`.
`save(show)` writes atomically (`.alfred.tmp` + `os.replace`) into a
folder that **must already exist** — the repository never invents a
folder name (the upstream `MediaOrganizer` is in charge of placing
files; the repo writes the sidecar next to them). `find_by_imdb_id` /
`find_all` walk `library_root/*/`, loading each readable sidecar;
folders without a sidecar return `None` / are skipped (no implicit
cold scan — that is the job of the upcoming `rescan_show` tool).
Corrupted YAML and schema violations are logged and skipped, never
raised, so a single bad folder does not break the rest of the
library. The repo keeps a tiny in-memory `imdb_id → folder_name`
index populated on every successful read/save, so subsequent saves
find the right destination without re-walking — useful when the show
folder name diverges from `show.get_folder_name()` (custom 1080p / 4K
variants). 20 integration tests on `tmp_path` cover the round-trip,
cold folder / unknown id returns, multi-show `find_all`, corrupted /
wrong-schema skipping, atomic write (no `.alfred.tmp` left behind),
overwrite, and folder-name fallbacks.
- **Sidecar ↔ TVShow bridge
(`alfred/infrastructure/persistence/dot_alfred/bridge.py`).**
`to_sidecar(show, folder_paths=...)` summarizes the rich domain
`AudioTrack` / `SubtitleTrack` to the sidecar's compact form (unique
audio languages in track order; subtitle entries derived from
`is_forced` and assumed `source="embedded"`). `from_sidecar(sidecar,
title=...)` reconstructs the domain `TVShow` with synthesized tracks
— one `AudioTrack` per language, one `SubtitleTrack` per entry, with
ffprobe-only fields (`codec`, `channels`, `channel_layout`) left as
`None`. The bridge is intentionally lossy on probe minutiae the
sidecar does not store; this is the documented trade-off from the
factual-only spec.
- **`.alfred` sidecar serializer
(`alfred/infrastructure/persistence/dot_alfred/`).** Implements step 2
of the `specs/dot_alfred.md` plan. Pure-dict in/out