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)