New orchestrator alfred.application.tv_shows.sync.sync_show calls
TMDBClient.get_tv_show_info, combines the response with the on-disk
release loaded via DotAlfredSeriesReleaseRepository.load_by_tmdb_id,
and upserts the result into DotAlfredTVShowLibraryIndex.
Policy:
* placeholders (auto-healed entries, status=="unknown") always
refresh regardless of TTL,
* fresh entries within Settings.tmdb_cache_ttl_days are no-ops,
* stale entries past TTL refresh,
* force=True overrides both gates,
* indexed shows whose per-show sidecar is gone still get a fresh
TMDB pass — slot map clears until rescan repopulates it,
* truly absent shows raise ShowNotFoundInLibrary from the new
alfred.application.exceptions module.
The Phase 4 walker + rescan logic classified seasons by parser
output (does the filename carry Exx?), but PACK vs EPISODIC is a
structural distinction:
* PACK = season folder with N flat SxxEyy videos directly inside
* EPISODIC = season folder with N subfolders, each holding one video
Changes:
* walker.py: descends two levels under show_root and classifies
each season folder by FS structure. SeasonFolder now carries
mode: ReleaseMode | None. Mixed layouts (flat + subfolders) and
EPISODIC subfolders with >1 video log a warning and report
mode=None.
* rescan.py: trusts walker.mode; drops the bogus 'single un-
numbered video → PACK with empty episodes' branch. A season
with no parseable episodes is now skipped with a warning.
* Tests rewritten against the real model: PACK with flat numbered
files, EPISODIC with one-video-per-subfolder, malformed mixed
layout skipped, single-un-numbered-file skipped.
Suite: 1237 → 1245 passing.
Rewrite rescan_show to build a SeriesRelease (Phase 1 v2 aggregate)
and persist it via DotAlfredSeriesReleaseRepository. The orchestrator
keeps reusing inspect_release as the single source of parse/probe
truth — only the assembly target changes (SeriesRelease/SeasonRelease/
EpisodeRelease instead of TVShow/Season/Episode).
New signature
rescan_show(
show_root,
*,
tmdb_id: TmdbId,
imdb_id: ImdbId | None = None,
series_repo: DotAlfredSeriesReleaseRepository,
scanner,
prober,
kb,
) -> SeriesRelease
Identity is TMDB-anchored (tmdb_id required, no coercion); imdb_id is
optional. No TMDB call from rescan — the library index auto-heals
from the new sidecar on its next read.
PACK vs EPISODIC
* Single-video + season-parsed + no-episode → SeasonRelease(
mode=PACK, folder=<season folder>, episodes=()). The slot map stays
empty until the Phase 5 TMDB sync supplies episode_count. We do
not fabricate an EpisodeRange we cannot prove on disk.
* Otherwise → EPISODIC: every file with (season, episode) becomes an
EpisodeRelease with EpisodeRange(start, end) = (E, E). Multi-episode
files (S01E01E02) still record only the first slot — Parser does
not yet expose episode_end (existing tech debt, unchanged).
Package move
The orchestrator moves from alfred/application/library/ to
alfred/application/tv_shows/ for symmetry with alfred/application/
movies/ (Step 2). walker.py + its tests move with it. The empty
library/ package is deleted.
Tests
tests/application/tv_shows/test_rescan.py rewritten end-to-end on
the real v2 repository, real KB, real scanner, stubbed prober.
9 happy-path + edge-case scenarios cover EPISODIC track flattening,
PACK empty-episodes semantics, sidecar round-trip, imdb_id optional,
empty show root, season folder with no videos, prober returning None.
test_walker.py moved verbatim (import path updated).
Full suite: 1214 passed / 10 skipped / 4 xfailed. The three v1
dot_alfred quarantines from Phase 3 stay in place until Step 3.