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:
@@ -1,11 +1,11 @@
|
||||
"""Tests for shared domain value objects: ImdbId, FilePath, FileSize."""
|
||||
"""Tests for shared domain value objects: ImdbId, TmdbId, FilePath, FileSize."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from alfred.domain.shared.exceptions import ValidationError
|
||||
from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId
|
||||
from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId, TmdbId
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ImdbId
|
||||
@@ -54,6 +54,53 @@ class TestImdbId:
|
||||
assert len(ids) == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TmdbId
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTmdbId:
|
||||
def test_valid_int(self):
|
||||
tid = TmdbId(84958)
|
||||
assert tid.value == 84958
|
||||
assert str(tid) == "84958"
|
||||
assert int(tid) == 84958
|
||||
|
||||
def test_zero_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
TmdbId(0)
|
||||
|
||||
def test_negative_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
TmdbId(-1)
|
||||
|
||||
def test_string_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
TmdbId("84958") # type: ignore[arg-type]
|
||||
|
||||
def test_float_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
TmdbId(84958.0) # type: ignore[arg-type]
|
||||
|
||||
def test_bool_raises(self):
|
||||
# bool is a subclass of int — make sure we reject it explicitly.
|
||||
with pytest.raises(ValidationError):
|
||||
TmdbId(True) # type: ignore[arg-type]
|
||||
with pytest.raises(ValidationError):
|
||||
TmdbId(False) # type: ignore[arg-type]
|
||||
|
||||
def test_repr(self):
|
||||
assert repr(TmdbId(84958)) == "TmdbId(84958)"
|
||||
|
||||
def test_equality(self):
|
||||
assert TmdbId(84958) == TmdbId(84958)
|
||||
assert TmdbId(84958) != TmdbId(12345)
|
||||
|
||||
def test_hashable(self):
|
||||
ids = {TmdbId(84958), TmdbId(12345), TmdbId(84958)}
|
||||
assert len(ids) == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FilePath
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user