diff --git a/core/listening_stats_worker.py b/core/listening_stats_worker.py index 42648a51..414f857a 100644 --- a/core/listening_stats_worker.py +++ b/core/listening_stats_worker.py @@ -90,15 +90,20 @@ class ListeningStatsWorker: logger.info("Listening stats worker thread started") # Build cache from existing data immediately (before first poll) - interruptible_sleep(self._stop_event, 5) + if interruptible_sleep(self._stop_event, 5): + return try: self._build_stats_cache() logger.info("Initial stats cache built from existing data") except Exception as e: logger.debug(f"Initial cache build skipped: {e}") + if self.should_stop: + return + # Wait before first poll - interruptible_sleep(self._stop_event, 10) + if interruptible_sleep(self._stop_event, 10): + return while not self.should_stop: try: diff --git a/core/media_scan_manager.py b/core/media_scan_manager.py index 451d5fed..f7661128 100644 --- a/core/media_scan_manager.py +++ b/core/media_scan_manager.py @@ -256,6 +256,8 @@ class MediaScanManager: if is_scanning: # Still scanning - trigger database update and continue periodic updates logger.info(f"{server_type.upper()} still scanning - triggering database update") + if self._shutting_down: + return self._call_completion_callbacks() # Schedule next periodic update @@ -268,6 +270,8 @@ class MediaScanManager: else: # Scanning stopped - final update and cleanup logger.info(f"{server_type.upper()} scanning completed - doing final database update") + if self._shutting_down: + return self._call_completion_callbacks() self._stop_periodic_updates() diff --git a/core/plex_scan_manager.py b/core/plex_scan_manager.py index 76df7dd5..fd8af490 100644 --- a/core/plex_scan_manager.py +++ b/core/plex_scan_manager.py @@ -174,6 +174,8 @@ class PlexScanManager: if is_scanning: # Still scanning - trigger database update and continue periodic updates logger.info("Plex still scanning - triggering database update") + if self._shutting_down: + return self._call_completion_callbacks() # Schedule next periodic update @@ -186,6 +188,8 @@ class PlexScanManager: else: # Scanning stopped - final update and cleanup logger.info("Plex scanning completed - doing final database update") + if self._shutting_down: + return self._call_completion_callbacks() self._stop_periodic_updates() diff --git a/core/web_scan_manager.py b/core/web_scan_manager.py index e13d2d72..d2110ae0 100644 --- a/core/web_scan_manager.py +++ b/core/web_scan_manager.py @@ -290,12 +290,19 @@ class WebScanManager: def _handle_scan_completion(self): """Handle scan completion and trigger callbacks""" - logger.info(f"Web {self._current_server_type.upper()} library scan completed") - - # Call completion callbacks - callbacks_to_call = [] with self._lock: + if self._shutting_down: + return + server_type = self._current_server_type callbacks_to_call = self._scan_completion_callbacks.copy() + downloads_during_scan = self._downloads_during_scan + + if not server_type: + logger.debug("Skipping web scan completion: no active server type") + self._reset_scan_state() + return + + logger.info(f"Web {server_type.upper()} library scan completed") for callback in callbacks_to_call: try: @@ -308,10 +315,9 @@ class WebScanManager: self._reset_scan_state() # Check if we need another scan due to downloads during this scan - with self._lock: - if self._downloads_during_scan: - logger.info("Web scan follow-up needed for downloads during scan") - self.request_scan("Follow-up scan for downloads during previous scan") + if downloads_during_scan: + logger.info("Web scan follow-up needed for downloads during scan") + self.request_scan("Follow-up scan for downloads during previous scan") def _reset_scan_state(self): """Reset internal scan state""" diff --git a/web_server.py b/web_server.py index fabfeb88..33f477c2 100644 --- a/web_server.py +++ b/web_server.py @@ -2638,6 +2638,8 @@ class WebUIDownloadMonitor: completed_tasks.append((batch_id, task_id)) # ---- All work below runs WITHOUT tasks_lock held ---- + if globals().get('IS_SHUTTING_DOWN', False) or not self.monitoring: + return # Execute deferred operations from _should_retry_task (network calls, nested locks) for op in deferred_ops: @@ -3460,6 +3462,11 @@ def _shutdown_runtime_components(): except Exception as e: print(f"Error saving API call history: {e}") + # Stop the active DB update worker before tearing down the executor it runs on. + # This lets an in-flight update observe should_stop and exit cleanly. + _stop_component(db_update_worker, "db update worker") + _stop_component(metadata_update_runtime_worker, "metadata update worker") + # Stop long-lived worker components in parallel so shutdown waits for the # slowest worker instead of serially burning the timeout for each one. _stop_components_parallel([ @@ -40789,6 +40796,7 @@ metadata_update_state = { } metadata_update_worker = None +metadata_update_runtime_worker = None metadata_update_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="metadata_update") # =============================== @@ -44903,7 +44911,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid): @app.route('/api/metadata/start', methods=['POST']) def start_metadata_update(): """Start the metadata update process - EXACT copy of dashboard.py logic""" - global metadata_update_worker, metadata_update_state + global metadata_update_worker, metadata_update_runtime_worker, metadata_update_state try: # Check if already running @@ -44971,6 +44979,7 @@ def start_metadata_update(): # Start the metadata update worker - EXACTLY like dashboard.py def run_metadata_update(): + global metadata_update_runtime_worker try: metadata_worker = WebMetadataUpdateWorker( None, # Artists will be loaded in the worker thread - EXACTLY like dashboard.py @@ -44979,12 +44988,15 @@ def start_metadata_update(): active_server, refresh_interval_days ) + metadata_update_runtime_worker = metadata_worker metadata_worker.run() except Exception as e: print(f"Error in metadata update worker: {e}") metadata_update_state['status'] = 'error' metadata_update_state['error'] = str(e) add_activity_item("", "Metadata Error", str(e), "Now") + finally: + metadata_update_runtime_worker = None metadata_update_worker = metadata_update_executor.submit(run_metadata_update)