|
|
|
|
@ -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):
|
|
|
|
|
"""
|
|
|
|
|
|