From 64d9db57ea562602eb72a12300f835140ca5048d Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Fri, 17 Apr 2026 20:06:24 +0300 Subject: [PATCH] More dead code removal after desktop app removal --- core/database_update_worker.py | 154 ++------------- core/plex_scan_manager.py | 342 --------------------------------- web_server.py | 8 +- 3 files changed, 24 insertions(+), 480 deletions(-) delete mode 100644 core/plex_scan_manager.py diff --git a/core/database_update_worker.py b/core/database_update_worker.py index 99eee8c0..77c23436 100644 --- a/core/database_update_worker.py +++ b/core/database_update_worker.py @@ -1,35 +1,5 @@ #!/usr/bin/env python3 -# Conditional PyQt6 import for backward compatibility with GUI version -try: - from PyQt6.QtCore import QThread, pyqtSignal - QT_AVAILABLE = True -except ImportError: - QT_AVAILABLE = False - # Define dummy classes for headless operation - class QThread: - def __init__(self): - self.callbacks = {} - def start(self): - import threading - self.thread = threading.Thread(target=self.run) - self.thread.daemon = True - self.thread.start() - def wait(self): - if hasattr(self, 'thread'): - self.thread.join() - def emit_signal(self, signal_name, *args): - if signal_name in self.callbacks: - for callback in self.callbacks[signal_name]: - try: - callback(*args) - except Exception as e: - logger.error(f"Error in callback for {signal_name}: {e}") - def connect_signal(self, signal_name, callback): - if signal_name not in self.callbacks: - self.callbacks[signal_name] = [] - self.callbacks[signal_name].append(callback) - import threading from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Optional, List, Callable @@ -42,32 +12,19 @@ from config.settings import config_manager logger = get_logger("database_update_worker") -class DatabaseUpdateWorker(QThread): - """Worker thread for updating SoulSync database with media server library data (Plex or Jellyfin)""" - - # Qt signals (only available when PyQt6 is installed) - if QT_AVAILABLE: - progress_updated = pyqtSignal(str, int, int, float) # current_item, processed, total, percentage - artist_processed = pyqtSignal(str, bool, str, int, int) # artist_name, success, details, albums_count, tracks_count - finished = pyqtSignal(int, int, int, int, int) # total_artists, total_albums, total_tracks, successful, failed - error = pyqtSignal(str) # error_message - phase_changed = pyqtSignal(str) # current_phase (artists, albums, tracks) +class DatabaseUpdateWorker: + """Worker for updating SoulSync database with media server library data.""" def __init__(self, media_client, database_path: str = "database/music_library.db", full_refresh: bool = False, server_type: str = "plex", force_sequential: bool = False): - super().__init__() - # Force sequential processing for web server mode to avoid threading issues self.force_sequential = force_sequential - - # Initialize signal callbacks for headless mode - if not QT_AVAILABLE: - self.callbacks = { - 'progress_updated': [], - 'artist_processed': [], - 'finished': [], - 'error': [], - 'phase_changed': [] - } + self.callbacks = { + 'progress_updated': [], + 'artist_processed': [], + 'finished': [], + 'error': [], + 'phase_changed': [], + } # Support both old plex_client parameter and new media_client parameter for backward compatibility if hasattr(media_client, '__class__') and 'plex' in media_client.__class__.__name__.lower(): @@ -116,19 +73,16 @@ class DatabaseUpdateWorker(QThread): self.database: Optional[MusicDatabase] = None def _emit_signal(self, signal_name: str, *args): - """Emit a signal in both Qt and headless modes""" - if QT_AVAILABLE and hasattr(self, signal_name): - # Qt mode - use actual signal - getattr(self, signal_name).emit(*args) - elif not QT_AVAILABLE: - # Headless mode - use callback system - self.emit_signal(signal_name, *args) + """Emit a signal through the callback registry.""" + for callback in self.callbacks.get(signal_name, []): + try: + callback(*args) + except Exception as e: + logger.error(f"Error in callback for {signal_name}: {e}") def connect_callback(self, signal_name: str, callback: Callable): - """Connect a callback for headless mode""" - if not QT_AVAILABLE: - self.connect_signal(signal_name, callback) - # In Qt mode, use the normal signal.connect() method + """Connect a callback for progress notifications.""" + self.callbacks.setdefault(signal_name, []).append(callback) def stop(self): """Stop the database update process""" @@ -1261,8 +1215,8 @@ class DatabaseUpdateWorker(QThread): logger.error(f"Error processing artist {getattr(artist, 'title', 'Unknown')}: {e}") return (getattr(artist, 'title', 'Unknown'), False, f"Error: {str(e)}", 0, 0) - # Process artists - use sequential processing in web server mode to avoid threading issues - if not QT_AVAILABLE or self.force_sequential: + # Process artists sequentially when requested (the web server uses this path). + if self.force_sequential: # Sequential processing for web server mode for i, artist in enumerate(artists): if self.should_stop: @@ -1277,7 +1231,7 @@ class DatabaseUpdateWorker(QThread): # Emit progress signal self._emit_signal('artist_processed', artist_name, success, details, album_count, track_count) else: - # Process artists in parallel using ThreadPoolExecutor (Qt mode only) + # Parallel processing for local/manual runs with ThreadPoolExecutor(max_workers=self.max_workers) as executor: # Submit all tasks future_to_artist = {executor.submit(process_single_artist, artist): artist @@ -1409,71 +1363,3 @@ class DatabaseUpdateWorker(QThread): except Exception as e: logger.error(f"Error in run_with_callback: {e}") - -class DatabaseStatsWorker(QThread): - """Simple worker for getting database statistics without blocking UI""" - - # Qt signals (only available when PyQt6 is installed) - if QT_AVAILABLE: - stats_updated = pyqtSignal(dict) # Database statistics - - def __init__(self, database_path: str = "database/music_library.db"): - super().__init__() - self.database_path = database_path - self.should_stop = False - - # Initialize signal callbacks for headless mode - if not QT_AVAILABLE: - self.callbacks = { - 'stats_updated': [] - } - - def stop(self): - """Stop the worker""" - self.should_stop = True - - def _emit_signal(self, signal_name: str, *args): - """Emit a signal in both Qt and headless modes""" - if QT_AVAILABLE and hasattr(self, signal_name): - # Qt mode - use actual signal - getattr(self, signal_name).emit(*args) - elif not QT_AVAILABLE: - # Headless mode - use callback system - self.emit_signal(signal_name, *args) - - def connect_callback(self, signal_name: str, callback: Callable): - """Connect a callback for headless mode""" - if not QT_AVAILABLE: - self.connect_signal(signal_name, callback) - # In Qt mode, use the normal signal.connect() method - - def run(self): - """Get database statistics and full info including last refresh""" - try: - if self.should_stop: - return - - database = get_database(self.database_path) - if self.should_stop: - return - - # Get database info for active server (server-aware statistics) - info = database.get_database_info_for_server() - if not self.should_stop: - self._emit_signal('stats_updated', info) - except Exception as e: - logger.error(f"Error getting database stats: {e}") - if not self.should_stop: - # Import here to avoid circular imports - from config.settings import config_manager - active_server = config_manager.get_active_media_server() - - self._emit_signal('stats_updated', { - 'artists': 0, - 'albums': 0, - 'tracks': 0, - 'database_size_mb': 0.0, - 'last_update': None, - 'last_full_refresh': None, - 'server_source': active_server - }) \ No newline at end of file diff --git a/core/plex_scan_manager.py b/core/plex_scan_manager.py deleted file mode 100644 index fd8af490..00000000 --- a/core/plex_scan_manager.py +++ /dev/null @@ -1,342 +0,0 @@ -#!/usr/bin/env python3 - -import threading -import time -from utils.logging_config import get_logger - -logger = get_logger("plex_scan_manager") - -class PlexScanManager: - """ - Smart Plex library scan manager with debouncing and scan-aware follow-up logic. - - Features: - - Debounces multiple scan requests to prevent spam - - Tracks downloads that happen during active scans - - Automatically triggers follow-up scans when needed - - Thread-safe operation - """ - - def __init__(self, plex_client, delay_seconds: int = 60): - """ - Initialize the scan manager. - - Args: - plex_client: PlexClient instance with trigger_library_scan method - delay_seconds: Debounce delay in seconds (default 60s) - """ - self.plex_client = plex_client - self.delay = delay_seconds - self._timer = None - self._scan_in_progress = False - self._downloads_during_scan = False - self._lock = threading.Lock() - self._scan_completion_callbacks = [] # List of callback functions to call when scan completes - self._scan_start_time = None # Track when scan started for timeout - self._max_scan_time = 1800 # Maximum scan time in seconds (30 minutes) - - # New periodic update system - self._periodic_update_timer = None # Timer for 5-minute periodic updates - self._periodic_update_interval = 300 # 5 minutes in seconds - self._is_doing_periodic_updates = False # Track if we're in periodic update mode - self._shutting_down = False - - logger.info(f"PlexScanManager initialized with {delay_seconds}s debounce delay") - - def request_scan(self, reason: str = "Download completed"): - """ - Request a library scan with smart debouncing logic. - - Args: - reason: Optional reason for the scan request (for logging) - """ - logger.info(f"DEBUG: Plex scan requested - reason: {reason}") - with self._lock: - if self._shutting_down: - logger.debug("Plex scan request ignored during shutdown") - return - if self._scan_in_progress: - # Plex is currently scanning - mark that we need another scan later - self._downloads_during_scan = True - logger.info(f"Plex scan in progress - queueing follow-up scan ({reason})") - return - - # Cancel any existing timer and start a new one - if self._timer: - self._timer.cancel() - logger.debug(f"Resetting scan timer ({reason})") - else: - logger.info(f"Plex scan queued - will execute in {self.delay}s ({reason})") - - # Start the debounce timer - self._timer = threading.Timer(self.delay, self._execute_scan) - self._timer.daemon = True - self._timer.start() - - def add_scan_completion_callback(self, callback): - """ - Add a callback function to be called when scan completes. - - Args: - callback: Function to call when scan completes (no arguments) - """ - with self._lock: - if callback not in self._scan_completion_callbacks: - self._scan_completion_callbacks.append(callback) - logger.info(f"DEBUG: Added scan completion callback: {callback.__name__}") - logger.info(f"DEBUG: Total callbacks registered: {len(self._scan_completion_callbacks)}") - - def remove_scan_completion_callback(self, callback): - """ - Remove a previously registered callback. - - Args: - callback: Function to remove from callbacks - """ - with self._lock: - if callback in self._scan_completion_callbacks: - self._scan_completion_callbacks.remove(callback) - logger.debug(f"Removed scan completion callback: {callback.__name__}") - - def _execute_scan(self): - """Execute the actual Plex library scan""" - with self._lock: - if self._shutting_down: - logger.debug("Plex scan execution skipped during shutdown") - return - if self._scan_in_progress: - logger.warning("Scan already in progress - skipping duplicate execution") - return - - self._scan_in_progress = True - self._downloads_during_scan = False - self._timer = None - self._scan_start_time = time.time() - - logger.info("Starting Plex library scan...") - - try: - success = self.plex_client.trigger_library_scan() - - if success: - logger.info("Plex library scan initiated successfully") - # Start new periodic update system instead of completion detection - self._start_periodic_updates() - else: - logger.error("Failed to initiate Plex library scan") - self._reset_scan_state() - - except Exception as e: - logger.error(f"Exception during Plex library scan: {e}") - self._reset_scan_state() - - def _start_periodic_updates(self): - """Start periodic database updates while Plex is scanning""" - try: - with self._lock: - if self._is_doing_periodic_updates: - logger.debug("Periodic updates already in progress") - return - - self._is_doing_periodic_updates = True - - logger.info(f"Starting periodic database updates - will check/update every {self._periodic_update_interval//60} minutes") - - # Schedule first periodic update after 5 minutes - self._periodic_update_timer = threading.Timer(self._periodic_update_interval, self._do_periodic_update) - self._periodic_update_timer.daemon = True - self._periodic_update_timer.start() - - except Exception as e: - logger.error(f"Error starting periodic updates: {e}") - self._reset_scan_state() - - def _do_periodic_update(self): - """Execute periodic database update and check if scanning continues""" - try: - with self._lock: - if not self._scan_in_progress: - logger.debug("Scan no longer in progress, stopping periodic updates") - return - - # Check for timeout - if self._scan_start_time and (time.time() - self._scan_start_time) > self._max_scan_time: - logger.warning(f"Plex scan timeout reached ({self._max_scan_time}s), stopping periodic updates") - self._stop_periodic_updates() - return - - # Check if Plex is still scanning - is_scanning = self.plex_client.is_library_scanning("Music") - elapsed_time = time.time() - self._scan_start_time if self._scan_start_time else 0 - - logger.info(f"PERIODIC UPDATE: After {elapsed_time//60:.0f} minutes - Plex scanning: {is_scanning}") - - 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 - if self._shutting_down: - return - logger.info(f"Scheduling next periodic update in {self._periodic_update_interval//60} minutes") - self._periodic_update_timer = threading.Timer(self._periodic_update_interval, self._do_periodic_update) - self._periodic_update_timer.daemon = True - self._periodic_update_timer.start() - 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() - - except Exception as e: - logger.error(f"Error during periodic update: {e}") - self._stop_periodic_updates() - - def _stop_periodic_updates(self): - """Stop periodic updates and clean up""" - try: - with self._lock: - self._is_doing_periodic_updates = False - - if self._periodic_update_timer: - self._periodic_update_timer.cancel() - self._periodic_update_timer = None - - logger.info("Stopped periodic database updates") - self._scan_completed() - - except Exception as e: - logger.error(f"Error stopping periodic updates: {e}") - - def _poll_scan_status(self): - """Poll Plex to check if library scan is still running""" - try: - with self._lock: - if not self._scan_in_progress: - logger.debug("Scan no longer in progress, stopping polling") - return - - # Check for timeout - if self._scan_start_time and (time.time() - self._scan_start_time) > self._max_scan_time: - logger.warning(f"Plex scan timeout reached ({self._max_scan_time}s), assuming completion") - self._scan_completed() - return - - # Check if Plex is still scanning - is_scanning = self.plex_client.is_library_scanning("Music") - logger.info(f"DEBUG: Plex scan status check - is_scanning: {is_scanning}") - - if is_scanning: - # Still scanning, poll again in 30 seconds - logger.info("DEBUG: Plex library still scanning, will check again in 30 seconds") - timer = threading.Timer(30, self._poll_scan_status) - timer.daemon = True - timer.start() - else: - # Scan completed! - elapsed_time = time.time() - self._scan_start_time if self._scan_start_time else 0 - logger.info(f"Plex library scan detected as completed (took {elapsed_time:.1f} seconds)") - self._scan_completed() - - except Exception as e: - logger.error(f"Error polling scan status: {e}") - # Fallback to assuming completion after error - self._scan_completed() - - def _scan_completed(self): - """Called when we assume the scan has completed""" - with self._lock: - was_in_progress = self._scan_in_progress - downloads_during_scan = self._downloads_during_scan - - # Reset scan state - self._scan_in_progress = False - - if not was_in_progress: - logger.debug("Scan completion callback called but scan was not in progress") - return - - logger.info("Plex library scan completed") - - # Call registered completion callbacks - self._call_completion_callbacks() - - # Check if we need a follow-up scan - if downloads_during_scan: - logger.info("Downloads occurred during scan - triggering follow-up scan") - self.request_scan("Follow-up scan for downloads during previous scan") - else: - logger.info("No downloads during scan - scan cycle complete") - - def _call_completion_callbacks(self): - """Call all registered scan completion callbacks""" - with self._lock: - callbacks = self._scan_completion_callbacks.copy() # Copy to avoid lock issues - - logger.info(f"DEBUG: Calling {len(callbacks)} scan completion callbacks") - for callback in callbacks: - try: - logger.info(f"DEBUG: Executing callback: {callback.__name__}") - callback() - logger.info(f"DEBUG: Callback {callback.__name__} completed successfully") - except Exception as e: - logger.error(f"Error in scan completion callback {callback.__name__}: {e}") - - def _reset_scan_state(self): - """Reset scan state after an error""" - with self._lock: - self._scan_in_progress = False - self._scan_start_time = None - - # Cancel periodic updates if running - if self._periodic_update_timer: - self._periodic_update_timer.cancel() - self._periodic_update_timer = None - self._is_doing_periodic_updates = False - - def force_scan(self): - """ - Force an immediate scan, bypassing debouncing. - Use sparingly - mainly for manual/administrative triggers. - """ - with self._lock: - if self._timer: - self._timer.cancel() - self._timer = None - - if self._scan_in_progress: - logger.warning("Force scan requested but scan already in progress") - return - - logger.info("Force scan requested - executing immediately") - self._execute_scan() - - def get_status(self) -> dict: - """Get current status of the scan manager""" - with self._lock: - return { - 'scan_in_progress': self._scan_in_progress, - 'downloads_during_scan': self._downloads_during_scan, - 'timer_active': self._timer is not None, - 'delay_seconds': self.delay - } - - def shutdown(self): - """Clean shutdown - cancel any pending timers""" - with self._lock: - self._shutting_down = True - if self._timer: - self._timer.cancel() - self._timer = None - - if self._periodic_update_timer: - self._periodic_update_timer.cancel() - self._periodic_update_timer = None - - self._is_doing_periodic_updates = False - logger.info("PlexScanManager shutdown - cancelled all pending timers") diff --git a/web_server.py b/web_server.py index 6bfbd473..76ed2deb 100644 --- a/web_server.py +++ b/web_server.py @@ -68,7 +68,7 @@ from core.soulseek_client import SoulseekClient from core.download_orchestrator import DownloadOrchestrator from core.tidal_client import TidalClient # Added import for Tidal from core.matching_engine import MusicMatchingEngine -from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker +from core.database_update_worker import DatabaseUpdateWorker from core.web_scan_manager import WebScanManager from core.lyrics_client import lyrics_client from core.metadata_cache import get_metadata_cache @@ -4066,7 +4066,7 @@ def _find_downloaded_file(download_path, track_data): return None # --- Refactored Logic from GUI Threads --- -# This logic is extracted from your QThread classes to be used directly by Flask. +# This logic is extracted from the database update worker to be used directly by Flask. def run_service_test(service, test_config): """ @@ -24219,7 +24219,7 @@ def _run_db_update_task(full_refresh, server_type): db_update_worker.connect_callback('finished', _db_update_finished_callback) db_update_worker.connect_callback('error', _db_update_error_callback) - # This is a blocking call that runs the QThread's logic + # This is a blocking call that runs the worker logic db_update_worker.run() @@ -24270,7 +24270,7 @@ def _run_deep_scan_task(server_type): def get_database_stats(): """Endpoint to get current database statistics.""" try: - # This logic is adapted from DatabaseStatsWorker + # This endpoint returns the same stats shape the UI expects. db = get_database() stats = db.get_database_info_for_server() return jsonify(stats)