fix(264): record library-level LastScan after a successful local scan
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 40s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m10s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

Local libraries permanently showed "Never scanned" in the SPA libraries
hub regardless of successful scans, even as item counts updated.

Root cause: ScanLocalLibraryHandler wrote only the path-level
LibraryPath.LastScan (which gates the per-path refresh interval) and
never the library-level Library.LastScan. The read API
(GetAllMediaSourcesForApiHandler) populates the hub's scan-time badge
from Library.LastScan, so that value stayed null forever. The three
remote scanners (Jellyfin/Emby/Plex) already set the library-level
value; only the local scanner did not. Both sides predate #202 — the
SPA hub merely made the missing value visible.

Mirror the remote scanners' semantics: record the library-level scan
time only when the scan actually ran and every path that ran succeeded,
so a skipped (unforced, interval not elapsed) or partially-failed scan
does not claim a successful scan time.

Also de-BOM the touched handler per the fix-as-you-touch charset gate
(#311).

fixes #264
This commit is contained in:
2026-07-17 14:18:14 +02:00
parent ed4b0d45b1
commit 5c8dd64acf
2 changed files with 188 additions and 1 deletions
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -65,6 +65,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
_scannerProxy.SetBaseUrl(baseUrl);
var scanned = false;
var anyFailed = false;
for (var i = 0; i < localLibrary.Paths.Count; i++)
{
@@ -145,6 +146,10 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
libraryPath.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(libraryPath);
}
else
{
anyFailed = true;
}
}
await _scannerProxy.UpdateProgress(progressMax, cancellationToken);
@@ -152,6 +157,15 @@ public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<
sw.Stop();
// the library-level LastScan drives the "last scanned" / "Never scanned" badge in the API and SPA,
// and is separate from the per-path LastScan that gates the refresh interval above. Only record it
// when every path that ran succeeded, mirroring the remote scanners (which set it solely on IsRight).
if (scanned && !anyFailed)
{
localLibrary.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(localLibrary);
}
if (scanned)
{
_logger.LogDebug(