From b23a3e91d8e818060146e1504c99f6a6cd8fb7f8 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 20 Jun 2026 13:44:35 -0700 Subject: [PATCH] video scan: show a 'cleaning up' phase during the deep-scan prune MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/video/scanner.py | 10 +++++++++- tests/test_video_scanner.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/video/scanner.py b/core/video/scanner.py index 3a8cf9e7..769334bb 100644 --- a/core/video/scanner.py +++ b/core/video/scanner.py @@ -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) diff --git a/tests/test_video_scanner.py b/tests/test_video_scanner.py index 456b170d..3afb08c1 100644 --- a/tests/test_video_scanner.py +++ b/tests/test_video_scanner.py @@ -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)