auto incremental update
This commit is contained in:
parent
b5aea8c038
commit
aee5995c1a
9 changed files with 419 additions and 3 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue