Media-server scanner: ratio-threshold + projection-failure detection for the empty-fetch guard #484
Closed
opened 2026-07-19 23:31:23 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
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.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#484
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.
Follow-up to #477 (PR #483), which added a deterministic zero-count anti-nuke guard to the three library-level media-server sweeps (
MediaServerReconciliationGuard). Two ideas from #477 were deliberately deferred there and recorded indocs/decisions.md:Ratio / missing-fraction threshold. #477's guard only fires when the incoming set is exactly empty. A fetch that returns a small non-zero subset of a large library (partial pagination failure, partial upstream corruption) would still flag the missing majority
FileNotFound. A "skip if missing fraction > X%" guard would catch that — but it risks suppressing a legitimate bulk deletion, so it needs a tunable, telemetry-backed policy (and a decision on the default threshold / whether it's configurable).Projection-failure detection.
JellyfinApiClient.GetPagedLibraryItemssilently drops items whoseProjectToShow/ProjectToEpisodereturns None (via.Somes()). Such an item the server did return is then indistinguishable from a deletion at the reconciliation step, so a projection regression could mass-flag healthy items. Fixing this needs a dropped-count threaded out of the API-client layer through to the scanner and used to suppress/limit the sweep.Both are larger, cross-layer changes than the zero-count guard. Not urgent (the zero-count guard covers the reported catastrophic whole-library case with no false positives).
Done-when
🔗 Session bundle — media-server scanner hardening: #460, #484, #491, #496, #500
Scan/reconcile data-integrity & anti-nuke robustness in the media-server sweeps, all carved from the #477/#488/#494/#497/#409 review chain. #491/#496/#500 share the check-then-insert / remove-stale+add-new reconcile idiom (duplicate rows on races or duplicate names / cross-library identity); #460 normalizes the MinValue LastScan write; #484 extends the empty-fetch guard to a ratio/projection-failure threshold. Same subsystem (Infrastructure/Scanner repositories + MediaServerReconciliationGuard) — sweepable in one session.
When you claim any member, check this cluster for co-workable siblings and sweep the disjoint set in one session (per docs/handoffs/chicorytv-issue-queue.md → bundles).
Claiming — top-ranked eligible backlog pickup from
scripts/select-queue.sh(prio-low tier, no active arc, deps clear, no prior claim).Scanning the media-server scanner-hardening bundle (#460/#484/#491/#496/#500) for co-workable siblings before starting; #460 and #500 are closed, so the live disjoint set is #484 + #491 (+ #496, which looks too large to sweep alongside).
Closing record
Outcome: Shipped in PR #612 (merged). Both deferred ideas resolved: the ratio/missing-fraction threshold is rejected with rationale, and projection-failure detection is implemented across all six media-server sweeps (movies, other videos, music videos, shows, and the nested per-show seasons and per-season episodes) for Jellyfin and Emby.
Root cause: n/a — this was deferred design work from #477, not a defect. But the sweep's underlying hazard is worth restating:
existing.Except(incoming)treats any absence as a deletion, andEmptyTrashHandlerdeletesFileNotFoundrows permanently, so anything that silently shrinksincomingis a data-loss vector.Decisions/conventions changed: added
scan.projection-failure-sweep-guard(sibling to, not superseding,scan.zero-item-fetch-guard— #477's rule is unchanged and still in force).Reusable knowledge: four things.
.Somes()was hiding two different failure classes. A guard-clauseNone(STRM file,LocationTypenon-FileSystem/Virtual, unknown item type) is a deliberate, permanent skip; aNonefrom acatchis a failure and is what's indistinguishable from a deletion. Only the second may suppress a sweep. Conflating them is the seductive wrong answer — it permanently disables reconciliation for any library holding a single STRM file, so stale rows accumulate forever.MediaSourcesempty andLocationType == "Virtual", not STRM. And Emby'sMediaSources-empty check is response-shape-dependent, unlike Jellyfin's genuinely permanent guards: a Refit/DTO drift presents as a mass Skip, which by construction leaves the sweep enabled — a total regression is caught by #477's zero branch, a partial one is not. Documented as a residual rather than re-classified, since re-classifying reintroduces STRM-style permanent suppression.Optionand nocatch, so a bad item throws and unwinds the scan rather than dropping silently.Verification: Scanner.Tests 1504, Infrastructure.Tests 114, ErsatzTV.Tests 1873, Core.Tests 652 (+1 skipped), Architecture.Tests 5 — all 0 failed. Negative controls run in both polarities (neutralizing the guard to
=> truefails exactly the refusal tests; to=> falsefails the positive controls). Join tests pin the same-counter-instance wiring at five places, so a refactor handing the client a fresh counter goes red rather than silently killing the protection. Full CI green. No live-E2E: scanner reconciliation logic, not an API write path or UI, and the failure mode requires a projection regression that can't be induced against a real server without shipping broken code.Deferred: #604 —
JellyfinCollectionScanner's remove-all-then-re-add overGetCollectionItemshas the identical seam and is still uncounted; a swallowed drop silently empties a collection. Not permanent media deletion, so genuinely lower priority, but the classification work is already done.Docs updated:
docs/decisions.md(+ regenerateddocs/decisions/README.mdcatalog). No route, endpoint, or SPA screen changed, soblazor-route-parity.md/api-conventions.md/spa-conventions.mdneeded no update.