video scan: show a 'cleaning up' phase during the deep-scan prune

On a deep scan the progress bar hits 100% when the last item is read, but the prune
(delete orphaned rows + cascades) runs AFTER that and — on a big cleanup — takes a
few seconds, during which the scan still reads as running (can't start a new one,
workers still paused). Looked stuck at 100%. Now the scanner sets a 'cleaning up
removed movies/shows' phase around the prune so the UI shows it's finalizing, not
frozen. Test spies the phase at prune time. 18 scanner tests pass.
This commit is contained in:
BoulderBadgeDad 2026-06-20 13:44:35 -07:00
parent 7cd289e7b5
commit b23a3e91d8
2 changed files with 28 additions and 1 deletions

View file

@ -189,7 +189,12 @@ class VideoLibraryScanner:
self._set(movies=movies, percent=pct())
self._set(movies=movies, percent=pct())
# Prune ONLY on a deep scan, and only when we actually saw items —
# so a transient empty response can never wipe the library.
# so a transient empty response can never wipe the library. The prune
# runs AFTER the bar fills, and a big cleanup (many orphaned rows +
# cascades) takes a few seconds — surface a phase so the UI shows
# "cleaning up", not a stuck 100%.
if do_prune and seen_movies:
self._set(phase="cleaning up removed movies", percent=pct())
removed_m = (self.db.prune_missing("movies", server, seen_movies)
if do_prune and seen_movies else 0)
@ -219,6 +224,9 @@ class VideoLibraryScanner:
episodes += sum(len(s.get("episodes", [])) for s in show.get("seasons", []))
processed += 1
self._set(shows=shows, episodes=episodes, percent=pct())
# Final prune (the one that delays "done" on a deep scan) — show it.
if do_prune and seen_shows:
self._set(phase="cleaning up removed shows", percent=100)
removed_s = (self.db.prune_missing("shows", server, seen_shows)
if do_prune and seen_shows else 0)

View file

@ -110,6 +110,25 @@ def test_deep_scan_prunes_removed_items(db):
assert db.dashboard_stats()["library"]["movies"] == 1
def test_deep_scan_shows_cleanup_phase_during_prune(db):
# On a deep scan the bar hits 100% before the (slow) prune runs — the scanner
# must surface a "cleaning up" phase so the UI doesn't look stuck at 100%.
scanner = VideoLibraryScanner(db)
scanner.scan_sync(lambda: FakeSource(
[{"server_id": "m1", "title": "A"}, {"server_id": "m2", "title": "B"}], []), mode="deep")
# Spy the phase at the exact moment prune is called on the next deep scan.
seen = {}
real_prune = db.prune_missing
def spy(table, server, ids):
seen[table] = scanner.get_status().get("phase")
return real_prune(table, server, ids)
db.prune_missing = spy
scanner.scan_sync(lambda: FakeSource([{"server_id": "m1", "title": "A"}], []), mode="deep")
assert seen.get("movies") == "cleaning up removed movies"
def test_full_refresh_does_not_prune(db):
# 'full' refreshes/adds but never removes — only 'deep' prunes.
scanner = VideoLibraryScanner(db)