Local libraries always show "Never scanned" — local scanner sets path-level LastScan but not library.LastScan #264
Closed
opened 2026-07-11 17:17:33 +02:00 by timothy
·
2 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#264
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.
Severity: Low (display). Pre-existing scanner bug, surfaced by the #202 SPA libraries hub during live E2E.
What's wrong
ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs:145setslibraryPath.LastScan = DateTime.UtcNow(path-level) after a successful scan but never setslibrary.LastScan(library-level). The read APIGetAllMediaSourcesForApiHandler(ErsatzTV.Application/MediaSources/Queries/GetAllMediaSourcesForApiHandler.cs:39) readslibrary.LastScanto populate the hub's scan-time / "Never scanned" badge — so every local library permanently shows "Never scanned" in the SPA regardless of successful scans, even as the item count updates correctly.Remote scanners do set the library-level value (e.g.
SynchronizeJellyfinLibraryByIdHandler.cs:92setsparameters.Library.LastScan = DateTime.UtcNow), so this is local-library-specific and reproduces every scan.Not a #202 regression
The read API + this scanner both predate #202; #202's SPA hub merely made the missing value visible. Filed as a follow-up, not a gate.
Fix
Set
library.LastScan = DateTime.UtcNowon the local library entity after a successful scan inScanLocalLibraryHandler(mirroring the remote scanners), and persist it. Add a scanner/handler test.Found via #202 live E2E (PR #262).
Done-when
ScanLocalLibraryHandlersets and persists library-levelLastScanafter a successful local scan, mirroring the remote scanners'IsRight-only semanticsErsatzTV.Scanner.Tests, proven non-vacuous via a negative control (test fails when the fix is inverted)Library.LastScan0001-01-01→2026-07-17 12:29:55, round-tripped throughGET /api/v1/media-sourcesReview-verdict: MERGEABLE @ 71708633/api/*, route, or convention change;docs/testing.mdupdated anyway to mapErsatzTV.Scanner.Tests(a gap this PR's new test file exposed)🔵 Claiming (Claude Code / Opus 4.8 orchestrator session, 2026-07-17).
Live claimability verified from raw Gitea state (not selector prose): OPEN, unclaimed,
priority: medium,GET /issues/264/dependencies→[](no openblocked-by).How the queue landed here. Tier 1 (open milestones) yielded nothing claimable: the Auto-Tune DetailPanel milestone is an umbrella (#383) whose children are either shipped (#384) or blocked behind in-flight #70 (#385, #386); the Scheduling milestone's #395 is blocked by #381 and #70 is claimed with PR #402 open; #59's milestone is now empty (closed this session — the arc it tracked has shipped). Tier 2 is empty: all five adversarial-reviewer audits (#20–#24) are claimed and carry posted findings deliverables. No open
priority: highexists, so the queue falls through topriority: medium, ordered by lowest issue number → #264. (#176 was the strict tier-1 winner but was explicitly deferred by the user this session.)Selector note (two errors caught, consistent with the standing lore): the cheap selector ranked #383 as winner without noticing it is a container, and reported audits #20–#23 as "unclaimed" when all four have posted deliverables. Re-derived the whole contested tier locally (fetch-all-open + filter on
.milestone.title, per-candidate/dependencies) rather than rechecking only the proposed winner.Plan: verify the diagnosis in the body still holds against current
mainbefore trusting it (the issue is 6 days old, and per our standing "recon before building" rule a stated root cause is a hypothesis until re-checked), then mirror the remote scanners' library-levelLastScanwrite inScanLocalLibraryHandler+ add a regression test. Adding a## Done-whensection to the body so the H6/H10 merge gate can derive consent.Closed — fixed and verified live
Merged as
5ac49e55(PR #408 →main), CI green at head71708633; branch deleted.Root cause (why it existed, not just what changed)
ScanLocalLibraryHandlerwrote only the path-levelLibraryPath.LastScan— the value that gates the per-path refresh interval — and never the library-levelLibrary.LastScan, which is the valueGetAllMediaSourcesForApiHandlerprojects into the SPA's scan-time badge. Two different fields, same name, different purposes: the one the scanner maintained was not the one the UI reads.Why only here: the three remote scanners (Jellyfin/Emby/Plex) each set the library-level value on success. The local scanner is the only one that scans per path rather than per library, and the library-level write was simply never added to that loop. I enumerated all four
LastScanwrite sites rather than trusting the cited line — 3 remote (correct) + 1 local (the bug). Both this handler and the read API predate #202; the #202 hub only made the gap visible, so it was never a #202 regression.What was done
ScanLocalLibraryHandlernow records library-levelLastScanwhen the scan actually ran and every path that ran succeeded — mirroring the remote scanners'IsRight-only semantics. A skipped (unforced, interval not elapsed) scan records nothing; a partially-failed scan records nothing at library level while successful paths still get their path-level timestamp.ErsatzTV.Scanner.Tests+ a logging assertion, all proven non-vacuous by negative control.Files changed
ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs(+ de-BOM'd per the #311 fix-as-you-touch gate)ErsatzTV.Scanner.Tests/Application/MediaSources/ScanLocalLibraryHandlerTests.cs(new)docs/testing.mdVerification
docs/e2e-local.mdscopes that to API write-path handlers — but run anyway, since this is an observable-badge bug): seeded a local Shows library, scanned via the real endpoint (HTTP 202, 2 episodes), and confirmedLibrary.LastScanwent0001-01-01→2026-07-17 12:29:55, round-tripping throughGET /api/v1/media-sources.MERGEABLE @ 71708633.The issue's premise was partly wrong (corrected in the body)
Live E2E disproved the stated symptom: an unscanned local library's
LastScanisDateTime.MinValue, notNULL— a fresh DB seeds 6 of 7 built-in local libraries with0001-01-01and only Remote Streams withNULL. Since the SPA branches on truthiness and MinValue serialises to a truthy string, such a library actually rendered "Last scan 12:00 AM", not "Never scanned". The root cause above was real and is fixed; the symptom description had been written from reading the code rather than the DB.Follow-ups filed (deferred deliberately, not dropped)
ScanCanceledis an ordinaryBaseError). Not fixed here because this PR made the local handler consistent with the three remote ones, which have behaved this way all along; a local-only fix would re-split them.Docs updated
docs/testing.md— added the missingErsatzTV.Scanner.Testsrow (the project has 1471 tests and was absent from a table that claims to be the authoritative map; this PR adding a file to it made the gap concrete) and documentedCore/FFmpeg/TranscodingTestsas[Explicit]/opt-in. No/api/*, route, or convention change, so no other doc applies.