Files
alfred/tests/application/movies/test_rescan.py
T
2026-05-26 21:45:11 +02:00

233 lines
7.4 KiB
Python

"""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"