"""Integration tests for ``rescan_movie``. Real filesystem (``tmp_path``), real release KB, real v2 movie repository. Only the media prober is stubbed — ffprobe needs real bytes and a binary. """ from __future__ import annotations from datetime import datetime from pathlib import Path import pytest from alfred.application.movies_TO_CHECK import MovieRescanFailed, rescan_movie from alfred.domain.shared_TO_CHECK.media import ( AudioTrack, MediaInfo, SubtitleTrack, VideoTrack, ) from alfred.domain.shared_TO_CHECK.value_objects import ImdbId, TmdbId from alfred.infrastructure.knowledge_TO_CHECK.release_kb import YamlReleaseKnowledge from alfred.infrastructure.persistence_TO_CHECK.dot_alfred.v2.repository import ( SIDECAR_FILENAME, DotAlfredMovieReleaseRepository, ) _KB = YamlReleaseKnowledge() _TMDB_ID = TmdbId(27205) _IMDB_ID = ImdbId("tt1375666") # --------------------------------------------------------------------------- # # Helpers # # --------------------------------------------------------------------------- # _MISSING = object() class _StubProber: def __init__(self, info=_MISSING) -> None: self._info: MediaInfo | None = ( _default_info() if info is _MISSING else info # type: ignore[assignment] ) self.calls: list[Path] = [] def probe(self, video: Path) -> MediaInfo | None: self.calls.append(video) return self._info def list_subtitle_streams(self, video: Path): # pragma: no cover return [] def _default_info() -> MediaInfo: return MediaInfo( video_tracks=(VideoTrack(index=0, codec="hevc", width=1920, height=1080),), audio_tracks=( AudioTrack( index=0, codec="eac3", channels=6, channel_layout="5.1", language="eng", ), ), subtitle_tracks=( SubtitleTrack( index=0, codec="subrip", language="eng", is_default=False, is_forced=False, ), ), ) def _make_inception_folder(root: Path) -> Path: """Build a fake Inception movie folder under ``root``.""" movie_dir = root / "Inception (2010)" movie_dir.mkdir() (movie_dir / "Inception.2010.1080p.BluRay.x264-GROUP.mkv").write_bytes(b"") return movie_dir def _library_with_movie(tmp_path: Path) -> tuple[Path, Path]: library = tmp_path / "movies" library.mkdir() movie_dir = _make_inception_folder(library) return library, movie_dir # --------------------------------------------------------------------------- # # Happy path # # --------------------------------------------------------------------------- # class TestHappyPath: def test_builds_release_with_tracks(self, tmp_path): library, movie_dir = _library_with_movie(tmp_path) repo = DotAlfredMovieReleaseRepository(library) prober = _StubProber() release = rescan_movie( movie_dir, tmdb_id=_TMDB_ID, imdb_id=_IMDB_ID, movie_repo=repo, prober=prober, kb=_KB, ) assert release.tmdb_id == _TMDB_ID assert release.imdb_id == _IMDB_ID assert release.folder == "Inception (2010)" assert str(release.file_path) == "Inception.2010.1080p.BluRay.x264-GROUP.mkv" assert isinstance(release.added_at, datetime) assert len(release.tracks.audio_tracks) == 1 assert release.tracks.audio_tracks[0].language == "eng" assert len(release.tracks.subtitle_tracks) == 1 def test_file_path_relative_to_movie_dir(self, tmp_path): library, movie_dir = _library_with_movie(tmp_path) repo = DotAlfredMovieReleaseRepository(library) release = rescan_movie( movie_dir, tmdb_id=_TMDB_ID, movie_repo=repo, prober=_StubProber(), kb=_KB, ) assert not Path(str(release.file_path)).is_absolute() def test_persists_sidecar_on_disk_and_round_trips(self, tmp_path): library, movie_dir = _library_with_movie(tmp_path) repo = DotAlfredMovieReleaseRepository(library) rescan_movie( movie_dir, tmdb_id=_TMDB_ID, imdb_id=_IMDB_ID, movie_repo=repo, prober=_StubProber(), kb=_KB, ) assert (movie_dir / SIDECAR_FILENAME).is_file() recovered = repo.find_by_tmdb_id(_TMDB_ID) assert recovered is not None assert recovered.tmdb_id == _TMDB_ID assert recovered.folder == "Inception (2010)" def test_probe_called_exactly_once(self, tmp_path): library, movie_dir = _library_with_movie(tmp_path) repo = DotAlfredMovieReleaseRepository(library) prober = _StubProber() rescan_movie( movie_dir, tmdb_id=_TMDB_ID, movie_repo=repo, prober=prober, kb=_KB, ) assert len(prober.calls) == 1 def test_imdb_id_optional(self, tmp_path): library, movie_dir = _library_with_movie(tmp_path) repo = DotAlfredMovieReleaseRepository(library) release = rescan_movie( movie_dir, tmdb_id=_TMDB_ID, movie_repo=repo, prober=_StubProber(), kb=_KB, ) assert release.imdb_id is None # --------------------------------------------------------------------------- # # Edge cases # # --------------------------------------------------------------------------- # class TestEdgeCases: def test_no_video_raises(self, tmp_path): library = tmp_path / "movies" library.mkdir() movie_dir = library / "Empty (2010)" movie_dir.mkdir() (movie_dir / "notes.txt").write_text("x") repo = DotAlfredMovieReleaseRepository(library) with pytest.raises(MovieRescanFailed): rescan_movie( movie_dir, tmdb_id=_TMDB_ID, movie_repo=repo, prober=_StubProber(), kb=_KB, ) def test_prober_returning_none_still_produces_release(self, tmp_path): library, movie_dir = _library_with_movie(tmp_path) repo = DotAlfredMovieReleaseRepository(library) release = rescan_movie( movie_dir, tmdb_id=_TMDB_ID, movie_repo=repo, prober=_StubProber(info=None), kb=_KB, ) assert release.tracks.audio_tracks == () assert release.tracks.subtitle_tracks == () def test_video_in_subfolder_is_found(self, tmp_path): library = tmp_path / "movies" library.mkdir() movie_dir = library / "Inception (2010)" movie_dir.mkdir() sub = movie_dir / "extras" sub.mkdir() (sub / "Inception.2010.1080p.mkv").write_bytes(b"") repo = DotAlfredMovieReleaseRepository(library) release = rescan_movie( movie_dir, tmdb_id=_TMDB_ID, movie_repo=repo, prober=_StubProber(), kb=_KB, ) # Relative path keeps the subfolder. assert str(release.file_path) == "extras/Inception.2010.1080p.mkv"