Scanner: FileNotFound does not cascade show → seasons → episodes, so playouts keep scheduling episodes of shows deleted upstream #476
Closed
opened 2026-07-19 20:59:10 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
renovate/nuget-sixlabors.imagesharp-vulnerability
913-scripted-playout-401
929-vuln-pkcs-imagesharp-v4
renovate/dotnet-monorepo
release/v26.15.0-notes
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#476
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while working #473 (2026-07-19). This is the scanner-side half that #473 deliberately left out of scope — #473 makes the streaming path survive a dead item; this issue is why dead items keep getting scheduled in the first place.
Symptom
On prod:
MediaItem.StateisFileNotFoundfor the show row of a show that was deleted from Jellyfin, but all 110 of its episodes remainRemoteOnly.The
TV Showslibrary was last scanned 2026-07-19 14:09, hours after the media disappeared — so this is not "no scan has run". The scan ran and left the episodes untouched.Root cause (traced)
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.csreconciles at three levels, each asexisting.Except(incomingItemIds)→FlagFileNotFound{Shows,Seasons,Episodes}:The season and episode sweeps run inside the per-show loop, over the shows present in the incoming list. When a show is gone from the media server it is absent from that list, so the loop never visits it and its seasons/episodes are never swept. Only the show row transitions to
FileNotFound; its children keep whatever state they last had — on Jellyfin that isRemoteOnly, becauseServerSupportsRemoteStreaming => true(JellyfinTelevisionLibraryScanner.cs:57).Why it matters
PlayoutBuilderfilters on the episode's ownState(ErsatzTV.Core/Scheduling/PlayoutBuilder.cs:1153—State is MediaItemState.FileNotFound or MediaItemState.Unavailable, gated onConfigElementKey.PlayoutSkipMissingItems). Because the episodes stayRemoteOnly, even turningPlayoutSkipMissingItemson does not exclude them — they keep getting scheduled, and every one of them is a guaranteed tune-in failure (that is exactly #473's reproduction).EmptyTrashHandlersearchesstate:FileNotFoundand deletes matches — that removes the show row and leaves 110 childless episode rows behind.Suggested fix
Cascade the flag: when a show is swept to
FileNotFound, sweep its seasons and episodes too (and likewise season → episodes). The repository already has the bulk-update primitives (JellyfinTelevisionRepository.FlagFileNotFoundShows/Seasons/Episodes,.cs:360/392/424), so this is most likely a matter of collecting the descendant ids for the swept shows and issuing the sameUPDATE MediaItem SET State = 1 WHERE Id IN @Ids— rather than new machinery.Verify the same gap on the Movie / OtherVideo sibling scanners and on Plex/Emby, which share the pattern.
Done-when
FileNotFoundon the next scanErsatzTV.Scanner.Tests(that project already substitutes the folder scanners + repositories and asserts the resulting repository writes)FileNotFound) — cascade mechanism proven by theTelevisionRepositoryCascadeTestsintegration test against the real schema; prod rescan of the stale rows deferred to post-deploy acceptance (operator: ersatztv-test after:latest, prod after a version tag)Claiming (Claude Code / Opus 4.8, orchestrator session). Working the scanner-side cascade: when a show is swept to
FileNotFound, cascade the flag to its seasons + episodes; will check the Movie/OtherVideo scanners and Plex/Emby siblings for the same gap, add aErsatzTV.Scanner.Testscase, and verify against the stale prod rows.PR #481 opened —
feat/476-cascade-filenotfound.Root cause confirmed exactly as traced in the issue: the TV scanner's season/episode
FlagFileNotFoundsweeps live inside the per-parent loops (ScanSeasons/ScanEpisodes), which only iterate parents present in the incoming list. A show gone from the media server is absent there → never visited → its seasons/episodes are never swept and keepRemoteOnly. I also found the sibling gap: a season removed while its show remains has the same problem for that season's episodes (the issue's "and likewise season → episodes").Fix: cascade by parent
MediaItem.Idvia two provider-agnostic repo helpers (FlagFileNotFoundSeasonsForShows,FlagFileNotFoundEpisodesForSeasons—Season.ShowId/Episode.SeasonIdare on the base tables, so no provider join). The scanner drives them after each parent sweep; all affected ids are reindexed. The shared abstract base fixes Jellyfin/Plex/Emby at once.Movie/OtherVideo: verified flat (single top-level sweep, no hierarchy) → no gap, unchanged.
Tests: Scanner.Tests asserts the scanner cascades through a substituted repo (proven non-vacuous — fails if the cascade calls are stripped); Integration tests exercise the real cascade SQL against the schema (flags only the targeted subtree, leaves a sibling show untouched, no-ops on empty input).
CI running; cold adversarial review in progress. Will verify the 717 stale prod rows via an operator rescan post-deploy.
Closed — merged via PR #481 (main
0d704255)What was done: cascade the
FileNotFoundflag from a removed parent to its descendants in the media-server TV scanner.Root cause: the season/episode
FlagFileNotFoundsweeps live inside the per-parent loops (ScanSeasons/ScanEpisodes), which only iterate parents present in the incoming server list. A show (or season) gone from the media server is never visited, so its descendants are never swept — they keepRemoteOnly, whichPlayoutBuilderdoesn't skip → guaranteed tune-in failures (#473 repro; 717 stale prod rows).Fix: two provider-agnostic repo helpers (
FlagFileNotFoundSeasonsForShows,FlagFileNotFoundEpisodesForSeasons— keyed on base-tableSeason.ShowId/Episode.SeasonId), driven by the scanner after each parent sweep (show→seasons→episodes; season→episodes). Shared abstract base fixes Jellyfin/Plex/Emby at once. Movie/OtherVideo scanners are flat → no gap, unchanged. I also fixed the sibling gap (removed season while show present) noted in the issue.Files changed:
IMediaServerTelevisionRepository.cs,{Jellyfin,Plex,Emby}TelevisionRepository.cs,MediaServerTelevisionLibraryScanner.cs, +MediaServerTelevisionLibraryScannerTests.cs(Scanner.Tests, non-vacuous) andTelevisionRepositoryCascadeTests.cs(Integration, real cascade SQL).Review: cold-context adversarial review → MERGEABLE, no correctness/safety bugs. Non-blocking notes accepted: LOW reindex-amplification (extends a pre-existing pattern; EmptyTrash removes such rows), NIT test-gap.
Deferred / follow-up: the prod rescan of the 717 stale rows is a post-deploy acceptance step — operator runs it on ersatztv-test after
:latest, and on prod after a version tag. The cascade mechanism itself is proven by the integration test.Docs: no docs-update trigger (scanner bug fix; no convention/route/API/decision change).