diff --git a/core/__pycache__/plex_client.cpython-312.pyc b/core/__pycache__/plex_client.cpython-312.pyc index 22ed4406..b3944cdc 100644 Binary files a/core/__pycache__/plex_client.cpython-312.pyc and b/core/__pycache__/plex_client.cpython-312.pyc differ diff --git a/core/__pycache__/plex_scan_manager.cpython-312.pyc b/core/__pycache__/plex_scan_manager.cpython-312.pyc index 5b2c26c3..1586e086 100644 Binary files a/core/__pycache__/plex_scan_manager.cpython-312.pyc and b/core/__pycache__/plex_scan_manager.cpython-312.pyc differ diff --git a/core/plex_client.py b/core/plex_client.py index 35bd490b..1658fa4c 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -645,6 +645,49 @@ class PlexClient: logger.error(f"Failed to trigger library scan for '{library_name}': {e}") return False + def is_library_scanning(self, library_name: str = "Music") -> bool: + """Check if Plex library is currently scanning""" + if not self.ensure_connection(): + logger.debug(f"🔍 DEBUG: Not connected to Plex, cannot check scan status") + return False + + try: + library = self.server.library.section(library_name) + + # Check if library has a scanning attribute or is refreshing + # The Plex API exposes this through the library's refreshing property + refreshing = hasattr(library, 'refreshing') and library.refreshing + logger.debug(f"🔍 DEBUG: Library.refreshing = {refreshing}") + + if refreshing: + logger.debug(f"🔍 DEBUG: Library is refreshing") + return True + + # Alternative method: Check server activities for scanning + try: + activities = self.server.activities() + logger.debug(f"🔍 DEBUG: Found {len(activities)} server activities") + + for activity in activities: + # Look for library scan activities + activity_type = getattr(activity, 'type', 'unknown') + activity_title = getattr(activity, 'title', 'unknown') + logger.debug(f"🔍 DEBUG: Activity - type: {activity_type}, title: {activity_title}") + + if (activity_type in ['library.scan', 'library.refresh'] and + library_name.lower() in activity_title.lower()): + logger.debug(f"🔍 DEBUG: Found matching scan activity: {activity_title}") + return True + except Exception as activities_error: + logger.debug(f"Could not check server activities: {activities_error}") + + logger.debug(f"🔍 DEBUG: No scan activity detected") + return False + + except Exception as e: + logger.debug(f"Error checking if library is scanning: {e}") + return False + def search_albums(self, album_name: str = "", artist_name: str = "", limit: int = 20) -> List[Dict[str, Any]]: """Search for albums in Plex library""" if not self.ensure_connection() or not self.music_library: diff --git a/core/plex_scan_manager.py b/core/plex_scan_manager.py index 06ca8602..185d754a 100644 --- a/core/plex_scan_manager.py +++ b/core/plex_scan_manager.py @@ -31,6 +31,9 @@ class PlexScanManager: 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) logger.info(f"PlexScanManager initialized with {delay_seconds}s debounce delay") @@ -41,6 +44,7 @@ class PlexScanManager: Args: reason: Optional reason for the scan request (for logging) """ + logger.info(f"🔍 DEBUG: Plex scan requested - reason: {reason}") with self._lock: if self._scan_in_progress: # Plex is currently scanning - mark that we need another scan later @@ -59,6 +63,31 @@ class PlexScanManager: self._timer = threading.Timer(self.delay, self._execute_scan) 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: @@ -69,6 +98,7 @@ class PlexScanManager: 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...") @@ -77,9 +107,8 @@ class PlexScanManager: if success: logger.info("✅ Plex library scan initiated successfully") - # Start a timer to check for follow-up scans - # Use a reasonable delay assuming scan takes at least 30 seconds - threading.Timer(30, self._scan_completed).start() + # Start polling to detect when scan actually completes + self._start_scan_monitoring() else: logger.error("❌ Failed to initiate Plex library scan") self._reset_scan_state() @@ -88,6 +117,49 @@ class PlexScanManager: logger.error(f"Exception during Plex library scan: {e}") self._reset_scan_state() + def _start_scan_monitoring(self): + """Start monitoring Plex scan status with polling""" + try: + # Initial delay before starting to poll (let scan actually start) + threading.Timer(15, self._poll_scan_status).start() + logger.debug("Started Plex scan status monitoring (30-second intervals, 30-minute timeout)") + except Exception as e: + logger.error(f"Error starting scan monitoring: {e}") + self._reset_scan_state() + + 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") + threading.Timer(30, self._poll_scan_status).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: @@ -103,6 +175,9 @@ class PlexScanManager: 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") @@ -110,10 +185,25 @@ class PlexScanManager: 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 def force_scan(self): """ diff --git a/ui/pages/__pycache__/artists.cpython-312.pyc b/ui/pages/__pycache__/artists.cpython-312.pyc index f40a3a7c..1e728b0e 100644 Binary files a/ui/pages/__pycache__/artists.cpython-312.pyc and b/ui/pages/__pycache__/artists.cpython-312.pyc differ diff --git a/ui/pages/__pycache__/sync.cpython-312.pyc b/ui/pages/__pycache__/sync.cpython-312.pyc index 350bf7f4..1fa2d716 100644 Binary files a/ui/pages/__pycache__/sync.cpython-312.pyc and b/ui/pages/__pycache__/sync.cpython-312.pyc differ diff --git a/ui/pages/artists.py b/ui/pages/artists.py index bc495642..93be5746 100644 --- a/ui/pages/artists.py +++ b/ui/pages/artists.py @@ -2996,6 +2996,96 @@ class ArtistsPage(QWidget): """Set the toast manager for showing notifications""" self.toast_manager = toast_manager + def _on_plex_scan_completed(self): + """Callback triggered when Plex scan completes - start automatic incremental database update""" + try: + # Import here to avoid circular imports + from database import get_database + from core.database_update_worker import DatabaseUpdateWorker + + # Check if we should run incremental update + if not self.plex_client or not self.plex_client.is_connected(): + logger.debug("Plex not connected - skipping automatic database update") + return + + # Check if database has a previous full refresh + database = get_database() + last_full_refresh = database.get_last_full_refresh() + if not last_full_refresh: + logger.info("No previous full refresh found - skipping automatic incremental update") + return + + # Check if database has sufficient content + try: + stats = database.get_database_info() + track_count = stats.get('tracks', 0) + + if track_count < 100: + logger.info(f"Database has only {track_count} tracks - skipping automatic incremental update") + return + except Exception as e: + logger.warning(f"Could not check database stats - skipping automatic update: {e}") + return + + # All conditions met - start incremental update + logger.info("🎵 Starting automatic incremental database update after Plex scan") + self._start_automatic_incremental_update() + + except Exception as e: + logger.error(f"Error in Plex scan completion callback: {e}") + + def _start_automatic_incremental_update(self): + """Start the automatic incremental database update""" + try: + from core.database_update_worker import DatabaseUpdateWorker + + # Avoid duplicate workers + if hasattr(self, '_auto_database_worker') and self._auto_database_worker and self._auto_database_worker.isRunning(): + logger.debug("Automatic database update already running") + return + + # Create worker for incremental update only + self._auto_database_worker = DatabaseUpdateWorker( + self.plex_client, + "database/music_library.db", + full_refresh=False # Always incremental for automatic updates + ) + + # Connect completion signal to log result + self._auto_database_worker.finished.connect(self._on_auto_update_finished) + self._auto_database_worker.error.connect(self._on_auto_update_error) + + # Start the update + self._auto_database_worker.start() + + except Exception as e: + logger.error(f"Error starting automatic incremental update: {e}") + + def _on_auto_update_finished(self, total_artists, total_albums, total_tracks, successful, failed): + """Handle completion of automatic database update""" + try: + if successful > 0: + logger.info(f"✅ Automatic database update completed: {successful} items processed successfully") + else: + logger.info("💡 Automatic database update completed - no new content found") + + # Clean up the worker + if hasattr(self, '_auto_database_worker'): + self._auto_database_worker.deleteLater() + delattr(self, '_auto_database_worker') + + except Exception as e: + logger.error(f"Error handling automatic update completion: {e}") + + def _on_auto_update_error(self, error_message): + """Handle error in automatic database update""" + logger.warning(f"Automatic database update encountered an error: {error_message}") + + # Clean up the worker + if hasattr(self, '_auto_database_worker'): + self._auto_database_worker.deleteLater() + delattr(self, '_auto_database_worker') + def setup_clients(self): """Initialize client connections""" try: @@ -3014,6 +3104,8 @@ class ArtistsPage(QWidget): # Initialize Plex scan manager now that clients are available if self.plex_client: self.scan_manager = PlexScanManager(self.plex_client, delay_seconds=60) + # Add automatic incremental database update after Plex scan completion + self.scan_manager.add_scan_completion_callback(self._on_plex_scan_completed) print("✅ PlexScanManager initialized for ArtistsPage") except Exception as e: diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index 84bdf3e5..c6b3712d 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -2377,6 +2377,14 @@ class DashboardPage(QWidget): 'soulseek_client': soulseek_client, 'downloads_page': downloads_page } + + # Initialize Plex scan manager for wishlist modal integration + self.scan_manager = None + if plex_client: + self.scan_manager = PlexScanManager(plex_client, delay_seconds=60) + # Add automatic incremental database update after Plex scan completion + self.scan_manager.add_scan_completion_callback(self._on_plex_scan_completed) + logger.info("✅ PlexScanManager initialized for Dashboard wishlist modal") def set_page_references(self, downloads_page, sync_page): """Called from main window to provide page references for live data""" @@ -2392,6 +2400,98 @@ class DashboardPage(QWidget): """Set the toast manager for showing notifications""" self.toast_manager = toast_manager + def _on_plex_scan_completed(self): + """Callback triggered when Plex scan completes - start automatic incremental database update""" + try: + # Import here to avoid circular imports + from database import get_database + from core.database_update_worker import DatabaseUpdateWorker + + # Check if we should run incremental update + plex_client = self.service_clients.get('plex_client') + if not plex_client or not plex_client.is_connected(): + logger.debug("Plex not connected - skipping automatic database update") + return + + # Check if database has a previous full refresh + database = get_database() + last_full_refresh = database.get_last_full_refresh() + if not last_full_refresh: + logger.info("No previous full refresh found - skipping automatic incremental update") + return + + # Check if database has sufficient content + try: + stats = database.get_database_info() + track_count = stats.get('tracks', 0) + + if track_count < 100: + logger.info(f"Database has only {track_count} tracks - skipping automatic incremental update") + return + except Exception as e: + logger.warning(f"Could not check database stats - skipping automatic update: {e}") + return + + # All conditions met - start incremental update + logger.info("🎵 Starting automatic incremental database update after Plex scan") + self._start_automatic_incremental_update() + + except Exception as e: + logger.error(f"Error in Plex scan completion callback: {e}") + + def _start_automatic_incremental_update(self): + """Start the automatic incremental database update""" + try: + from core.database_update_worker import DatabaseUpdateWorker + + # Avoid duplicate workers + if hasattr(self, '_auto_database_worker') and self._auto_database_worker and self._auto_database_worker.isRunning(): + logger.debug("Automatic database update already running") + return + + # Create worker for incremental update only + plex_client = self.service_clients.get('plex_client') + self._auto_database_worker = DatabaseUpdateWorker( + plex_client, + "database/music_library.db", + full_refresh=False # Always incremental for automatic updates + ) + + # Connect completion signal to log result + self._auto_database_worker.finished.connect(self._on_auto_update_finished) + self._auto_database_worker.error.connect(self._on_auto_update_error) + + # Start the update + self._auto_database_worker.start() + + except Exception as e: + logger.error(f"Error starting automatic incremental update: {e}") + + def _on_auto_update_finished(self, total_artists, total_albums, total_tracks, successful, failed): + """Handle completion of automatic database update""" + try: + if successful > 0: + logger.info(f"✅ Automatic database update completed: {successful} items processed successfully") + else: + logger.info("💡 Automatic database update completed - no new content found") + + # Clean up the worker + if hasattr(self, '_auto_database_worker'): + self._auto_database_worker.deleteLater() + delattr(self, '_auto_database_worker') + + except Exception as e: + logger.error(f"Error handling automatic update completion: {e}") + + def _on_auto_update_error(self, error_message): + """Handle error in automatic database update""" + logger.warning(f"Automatic database update encountered an error: {error_message}") + + # Clean up the worker + if hasattr(self, '_auto_database_worker'): + self._auto_database_worker.deleteLater() + delattr(self, '_auto_database_worker') + def setup_ui(self): self.setStyleSheet(""" DashboardPage { diff --git a/ui/pages/sync.py b/ui/pages/sync.py index b1adeb48..babedeaf 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -1994,6 +1994,8 @@ class SyncPage(QWidget): self.scan_manager = None if self.plex_client: self.scan_manager = PlexScanManager(self.plex_client, delay_seconds=60) + # Add automatic incremental database update after Plex scan completion + self.scan_manager.add_scan_completion_callback(self._on_plex_scan_completed) self.setup_ui() @@ -2005,6 +2007,95 @@ class SyncPage(QWidget): """Set the toast manager for showing notifications""" self.toast_manager = toast_manager + def _on_plex_scan_completed(self): + """Callback triggered when Plex scan completes - start automatic incremental database update""" + try: + # Import here to avoid circular imports + from database import get_database + from core.database_update_worker import DatabaseUpdateWorker + + # Check if we should run incremental update + if not self.plex_client or not self.plex_client.is_connected(): + logger.debug("Plex not connected - skipping automatic database update") + return + + # Check if database has a previous full refresh + database = get_database() + last_full_refresh = database.get_last_full_refresh() + if not last_full_refresh: + logger.info("No previous full refresh found - skipping automatic incremental update") + return + + # Check if database has sufficient content + try: + stats = database.get_database_info() + track_count = stats.get('tracks', 0) + + if track_count < 100: + logger.info(f"Database has only {track_count} tracks - skipping automatic incremental update") + return + except Exception as e: + logger.warning(f"Could not check database stats - skipping automatic update: {e}") + return + + # All conditions met - start incremental update + logger.info("🎵 Starting automatic incremental database update after Plex scan") + self._start_automatic_incremental_update() + + except Exception as e: + logger.error(f"Error in Plex scan completion callback: {e}") + + def _start_automatic_incremental_update(self): + """Start the automatic incremental database update""" + try: + from core.database_update_worker import DatabaseUpdateWorker + + # Avoid duplicate workers + if hasattr(self, '_auto_database_worker') and self._auto_database_worker and self._auto_database_worker.isRunning(): + logger.debug("Automatic database update already running") + return + + # Create worker for incremental update only + self._auto_database_worker = DatabaseUpdateWorker( + self.plex_client, + "database/music_library.db", + full_refresh=False # Always incremental for automatic updates + ) + + # Connect completion signal to log result + self._auto_database_worker.finished.connect(self._on_auto_update_finished) + self._auto_database_worker.error.connect(self._on_auto_update_error) + + # Start the update + self._auto_database_worker.start() + + except Exception as e: + logger.error(f"Error starting automatic incremental update: {e}") + + def _on_auto_update_finished(self, total_artists, total_albums, total_tracks, successful, failed): + """Handle completion of automatic database update""" + try: + if successful > 0: + logger.info(f"✅ Automatic database update completed: {successful} items processed successfully") + else: + logger.info("💡 Automatic database update completed - no new content found") + + # Clean up the worker + if hasattr(self, '_auto_database_worker'): + self._auto_database_worker.deleteLater() + delattr(self, '_auto_database_worker') + + except Exception as e: + logger.error(f"Error handling automatic update completion: {e}") + + def _on_auto_update_error(self, error_message): + """Handle error in automatic database update""" + logger.warning(f"Automatic database update encountered an error: {error_message}") + + # Clean up the worker + if hasattr(self, '_auto_database_worker'): + self._auto_database_worker.deleteLater() + delattr(self, '_auto_database_worker') def _update_and_save_sync_status(self, playlist_id, result, snapshot_id): """Updates the sync status for a given playlist and saves to file."""