refactor(domain): freeze Movie and Episode, switch track collections to tuple

Movie and Episode become @dataclass(frozen=True, eq=False), with
audio_tracks/subtitle_tracks held as tuple[...] instead of list[...].
Identity-based equality is preserved via the existing __eq__/__hash__.
__post_init__ coercion (imdb_id, title, season_number, episode_number)
uses object.__setattr__ to stay compatible with frozen.

The MediaWithTracks mixin contract is updated to tuple accordingly.

Callers projecting enrichment results (probe output, file metadata) now
rebuild via dataclasses.replace(...) — same pattern recently adopted for
ParsedRelease.

Season and TVShow stay mutable for now: freezing the aggregate root
would cascade a full reconstruction on every add_episode, deferred.
This commit is contained in:
2026-05-21 13:40:22 +02:00
parent 3dc73a5214
commit 02e478a157
4 changed files with 39 additions and 16 deletions
+13
View File
@@ -77,6 +77,19 @@ callers).
### Changed
- **`Movie` and `Episode` are now frozen dataclasses.** Both entities
hold their track collections as `tuple[AudioTrack, ...]` and
`tuple[SubtitleTrack, ...]` instead of mutable lists, and are
`@dataclass(frozen=True, eq=False)` (identity-based equality
preserved via `__eq__`/`__hash__`). `__post_init__` coercion uses
`object.__setattr__` for the `imdb_id` / `title` /
`season_number` / `episode_number` normalizations. To project
enrichment results (probe output, file metadata) callers now rebuild
via `dataclasses.replace(...)`. Pattern aligned with the recent
`ParsedRelease` freeze. `MediaWithTracks` mixin contract updated to
`tuple` accordingly. `Season` and `TVShow` remain mutable for now —
freezing the aggregate root would cascade a full reconstruction on
every `add_episode`, deferred.
- **`SubtitleCandidate` renamed to `SubtitleScanResult`.** The old name
conflated "this might become a placed subtitle" with "this is what a
scan pass produced". The class is the output of a scan/identify pass