2f160644da
Phase 3 prep: Movie aggregate is about to become TMDB-only (no filesystem fields). added_at is a release-time observation, not a TMDB-aggregate concern, so it moves to MovieRelease + MovieReleaseSidecar. - Add added_at: datetime (required) to MovieRelease with a type-check in __post_init__. - Add added_at: datetime (required) to MovieReleaseSidecar. - Bump SCHEMA_VERSION 1 → 2 with a version-history note. - Bridge round-trips added_at via Pydantic mode="json" (datetime → ISO 8601 string). - Tests: update MovieRelease fixtures, add a validator test, add an added_at round-trip test, switch hard-coded `1` assertions to SCHEMA_VERSION for future-proofing. No v1 sidecars in the wild yet — no migration code needed.
202 lines
6.5 KiB
Python
202 lines
6.5 KiB
Python
"""Shared fixtures for v2 ``.alfred`` integration tests.
|
|
|
|
The fixtures here build realistic ``SeriesRelease`` / ``MovieRelease``
|
|
aggregates — populated tracks, multi-episode files, both PACK and
|
|
EPISODIC modes — so every test starts from a known-rich state.
|
|
The point is to make round-trip tests genuinely lossless-checking
|
|
(if a field is unused in the fixture, the round-trip can't prove
|
|
much about it).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
|
|
from alfred.domain.releases.entities import (
|
|
EpisodeRelease,
|
|
MovieRelease,
|
|
SeasonRelease,
|
|
SeriesRelease,
|
|
TrackProfile,
|
|
)
|
|
from alfred.domain.releases.value_objects import EpisodeRange, ReleaseMode
|
|
from alfred.domain.shared.media import AudioTrack, SubtitleTrack
|
|
from alfred.domain.shared.value_objects import FilePath, ImdbId, TmdbId
|
|
from alfred.domain.tv_shows.value_objects import EpisodeNumber, SeasonNumber
|
|
from alfred.infrastructure.api.tmdb.dto import TmdbSeasonInfo, TmdbShowInfo
|
|
|
|
|
|
def _audio(lang: str = "eng", *, index: int = 0) -> AudioTrack:
|
|
# ``index`` defaults to 0 to match what the bridge reconstructs on
|
|
# read (sidecars don't persist ffprobe stream indices — see the
|
|
# bridge module's track-conversion notes). Pass explicit indices
|
|
# only when a fixture has multiple tracks of the same kind.
|
|
return AudioTrack(
|
|
index=index,
|
|
codec="eac3",
|
|
channels=6,
|
|
channel_layout="5.1",
|
|
language=lang,
|
|
)
|
|
|
|
|
|
def _sub(
|
|
lang: str = "eng",
|
|
*,
|
|
index: int = 0,
|
|
forced: bool = False,
|
|
sdh: bool = False,
|
|
) -> SubtitleTrack:
|
|
return SubtitleTrack(
|
|
index=index,
|
|
codec="subrip",
|
|
language=lang,
|
|
is_default=False,
|
|
is_forced=forced,
|
|
is_sdh=sdh,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def foundation_release() -> SeriesRelease:
|
|
"""Foundation S01 (PACK, 3 files) + S02 (EPISODIC, one multi-episode file)."""
|
|
s01 = SeasonRelease(
|
|
season_number=SeasonNumber(1),
|
|
folder="Foundation.S01.1080p.WEBRip.x265-RARBG",
|
|
mode=ReleaseMode.PACK,
|
|
episodes=(
|
|
EpisodeRelease(
|
|
episodes=EpisodeRange(EpisodeNumber(1), EpisodeNumber(1)),
|
|
file_path=FilePath(
|
|
"Foundation.S01.1080p.WEBRip.x265-RARBG/"
|
|
"Foundation.S01E01.1080p.WEBRip.x265-RARBG.mkv"
|
|
),
|
|
tracks=TrackProfile(
|
|
audio_tracks=(_audio("eng"),),
|
|
subtitle_tracks=(
|
|
_sub("eng", index=0),
|
|
_sub("eng", index=1, sdh=True),
|
|
),
|
|
),
|
|
),
|
|
EpisodeRelease(
|
|
episodes=EpisodeRange(EpisodeNumber(2), EpisodeNumber(2)),
|
|
file_path=FilePath(
|
|
"Foundation.S01.1080p.WEBRip.x265-RARBG/"
|
|
"Foundation.S01E02.1080p.WEBRip.x265-RARBG.mkv"
|
|
),
|
|
tracks=TrackProfile(audio_tracks=(_audio("eng"),)),
|
|
),
|
|
EpisodeRelease(
|
|
episodes=EpisodeRange(EpisodeNumber(3), EpisodeNumber(3)),
|
|
file_path=FilePath(
|
|
"Foundation.S01.1080p.WEBRip.x265-RARBG/"
|
|
"Foundation.S01E03.1080p.WEBRip.x265-RARBG.mkv"
|
|
),
|
|
tracks=TrackProfile(audio_tracks=(_audio("eng"),)),
|
|
),
|
|
),
|
|
)
|
|
s02 = SeasonRelease(
|
|
season_number=SeasonNumber(2),
|
|
folder="Foundation.S02",
|
|
mode=ReleaseMode.EPISODIC,
|
|
episodes=(
|
|
EpisodeRelease(
|
|
episodes=EpisodeRange(EpisodeNumber(1), EpisodeNumber(1)),
|
|
file_path=FilePath(
|
|
"Foundation.S02/Foundation.S02E01.1080p.x265-ELiTE/"
|
|
"Foundation.S02E01.1080p.x265-ELiTE.mkv"
|
|
),
|
|
tracks=TrackProfile(audio_tracks=(_audio("eng"),)),
|
|
),
|
|
# Multi-episode file (E02 + E03 in one .mkv).
|
|
EpisodeRelease(
|
|
episodes=EpisodeRange(EpisodeNumber(2), EpisodeNumber(3)),
|
|
file_path=FilePath(
|
|
"Foundation.S02/Foundation.S02E02-E03.2160p.WEB.x265-CtrlHD/"
|
|
"Foundation.S02E02-E03.2160p.WEB.x265-CtrlHD.mkv"
|
|
),
|
|
tracks=TrackProfile(
|
|
audio_tracks=(_audio("eng"),),
|
|
subtitle_tracks=(_sub("eng", forced=True),),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
return SeriesRelease(
|
|
tmdb_id=TmdbId(84958),
|
|
imdb_id=ImdbId("tt0804484"),
|
|
seasons=(s01, s02),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def inception_release() -> MovieRelease:
|
|
"""Inception (2010) — single-file movie with rich tracks."""
|
|
return MovieRelease(
|
|
tmdb_id=TmdbId(27205),
|
|
imdb_id=ImdbId("tt1375666"),
|
|
folder="Inception.2010.1080p.BluRay.x264-GROUP",
|
|
file_path=FilePath("Inception.2010.1080p.BluRay.x264-GROUP.mkv"),
|
|
added_at=datetime(2026, 5, 25, 8, 30, 0, tzinfo=UTC),
|
|
tracks=TrackProfile(
|
|
audio_tracks=(
|
|
AudioTrack(
|
|
index=0,
|
|
codec="dts",
|
|
channels=8,
|
|
channel_layout="7.1",
|
|
language="eng",
|
|
),
|
|
),
|
|
subtitle_tracks=(
|
|
_sub("eng", index=0),
|
|
_sub("fre", index=1, forced=True),
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def foundation_tmdb_info() -> TmdbShowInfo:
|
|
"""Foundation TMDB cache snapshot — 3 seasons, S03 not yet aired."""
|
|
return TmdbShowInfo(
|
|
tmdb_id=84958,
|
|
imdb_id="tt0804484",
|
|
name="Foundation",
|
|
status="Returning Series",
|
|
seasons=(
|
|
TmdbSeasonInfo(number=1, episode_count=10, aired=True),
|
|
TmdbSeasonInfo(number=2, episode_count=10, aired=True),
|
|
TmdbSeasonInfo(number=3, episode_count=10, aired=False),
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def now_utc() -> datetime:
|
|
"""Stable UTC reference for deterministic fetched_at fields."""
|
|
return datetime(2026, 5, 25, 8, 30, 0, tzinfo=UTC)
|
|
|
|
|
|
@pytest.fixture
|
|
def tv_library(tmp_path):
|
|
"""Empty ``tv_shows/`` directory pre-populated with show folders."""
|
|
root = tmp_path / "tv_shows"
|
|
root.mkdir()
|
|
(root / "Foundation").mkdir()
|
|
(root / "Fallout").mkdir()
|
|
return root
|
|
|
|
|
|
@pytest.fixture
|
|
def movie_library(tmp_path):
|
|
"""Empty ``movies/`` directory pre-populated with one movie folder."""
|
|
root = tmp_path / "movies"
|
|
root.mkdir()
|
|
(root / "Inception.2010.1080p.BluRay.x264-GROUP").mkdir()
|
|
return root
|