feat(dot_alfred): load_by_tmdb_id on release repos

Series repo returns (release, folder) so the upcoming sync
orchestrator can feed the library index's upsert(..., path=...).
Movie repo returns the release alone (folder is on release.folder
by the one-folder-one-file convention) — kept as a semantic alias
of find_by_tmdb_id for symmetry with the series side.
This commit is contained in:
2026-05-26 00:45:14 +02:00
parent 0dc053881a
commit 1efe9a82c1
2 changed files with 65 additions and 0 deletions
@@ -117,6 +117,21 @@ class DotAlfredSeriesReleaseRepository:
return release
return None
def load_by_tmdb_id(
self, tmdb_id: TmdbId
) -> tuple[SeriesRelease, str] | None:
"""Return ``(release, show_folder_name)`` for ``tmdb_id``, or ``None``.
Same lookup as :meth:`find_by_tmdb_id` but also returns the
folder name the release lives in — needed by the upcoming
sync orchestrator to feed the library index's ``upsert(...,
path=...)``.
"""
for folder, release in self._iter_library():
if release.tmdb_id == tmdb_id:
return release, folder
return None
def find_all(self) -> list[SeriesRelease]:
"""Return every readable release under ``library_root/``.
@@ -198,6 +213,18 @@ class DotAlfredMovieReleaseRepository:
return release
return None
def load_by_tmdb_id(self, tmdb_id: TmdbId) -> MovieRelease | None:
"""Return the :class:`MovieRelease` for ``tmdb_id``, or ``None``.
Movies carry their folder on ``release.folder`` (one-folder-
one-file convention), so no separate folder name is returned —
this is just a semantic alias of :meth:`find_by_tmdb_id`
provided for symmetry with
:meth:`DotAlfredSeriesReleaseRepository.load_by_tmdb_id`, the
sync orchestrators rely on the same name on both sides.
"""
return self.find_by_tmdb_id(tmdb_id)
def find_all(self) -> list[MovieRelease]:
return [release for _folder, release in self._iter_library()]
@@ -90,6 +90,30 @@ class TestSeriesReleaseRepositoryReads:
assert len(results) == 1
class TestSeriesReleaseRepositoryLoadByTmdbId:
def test_returns_release_and_folder_tuple(
self, tv_library, foundation_release
):
repo = DotAlfredSeriesReleaseRepository(tv_library)
repo.save(foundation_release, show_folder="Foundation")
result = repo.load_by_tmdb_id(TmdbId(84958))
assert result is not None
release, folder = result
assert release == foundation_release
assert folder == "Foundation"
def test_returns_none_when_tmdb_id_absent(
self, tv_library, foundation_release
):
repo = DotAlfredSeriesReleaseRepository(tv_library)
repo.save(foundation_release, show_folder="Foundation")
assert repo.load_by_tmdb_id(TmdbId(999999)) is None
def test_returns_none_on_empty_library(self, tv_library):
repo = DotAlfredSeriesReleaseRepository(tv_library)
assert repo.load_by_tmdb_id(TmdbId(84958)) is None
class TestSeriesReleaseRepositoryDelete:
def test_delete_removes_sidecar(self, tv_library, foundation_release):
repo = DotAlfredSeriesReleaseRepository(tv_library)
@@ -123,6 +147,20 @@ class TestMovieReleaseRepository:
restored = repo.find_by_tmdb_id(TmdbId(27205))
assert restored == inception_release
def test_load_by_tmdb_id_returns_release(
self, movie_library, inception_release
):
repo = DotAlfredMovieReleaseRepository(movie_library)
repo.save(inception_release)
assert repo.load_by_tmdb_id(TmdbId(27205)) == inception_release
def test_load_by_tmdb_id_returns_none_when_absent(
self, movie_library, inception_release
):
repo = DotAlfredMovieReleaseRepository(movie_library)
repo.save(inception_release)
assert repo.load_by_tmdb_id(TmdbId(999999)) is None
def test_anchor_mismatch_logs_warning(
self, movie_library, inception_release, caplog
):