bug fixes
This commit is contained in:
parent
906d0c23b7
commit
73679f5709
8 changed files with 203 additions and 0 deletions
Binary file not shown.
BIN
core/__pycache__/plex_scan_manager.cpython-312.pyc
Normal file
BIN
core/__pycache__/plex_scan_manager.cpython-312.pyc
Normal file
Binary file not shown.
|
|
@ -10,6 +10,7 @@ from datetime import datetime, timedelta
|
|||
import re
|
||||
from utils.logging_config import get_logger
|
||||
from config.settings import config_manager
|
||||
import threading
|
||||
|
||||
logger = get_logger("plex_client")
|
||||
|
||||
|
|
@ -630,6 +631,20 @@ class PlexClient:
|
|||
logger.error(f"Error updating track metadata: {e}")
|
||||
return False
|
||||
|
||||
def trigger_library_scan(self, library_name: str = "Music") -> bool:
|
||||
"""Trigger Plex library scan for the specified library"""
|
||||
if not self.ensure_connection():
|
||||
return False
|
||||
|
||||
try:
|
||||
library = self.server.library.section(library_name)
|
||||
library.update() # Non-blocking scan request
|
||||
logger.info(f"🎵 Triggered Plex library scan for '{library_name}'")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to trigger library scan for '{library_name}': {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:
|
||||
|
|
|
|||
151
core/plex_scan_manager.py
Normal file
151
core/plex_scan_manager.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#!/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()
|
||||
|
||||
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)
|
||||
"""
|
||||
with self._lock:
|
||||
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.start()
|
||||
|
||||
def _execute_scan(self):
|
||||
"""Execute the actual Plex library scan"""
|
||||
with self._lock:
|
||||
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
|
||||
|
||||
logger.info("🎵 Starting Plex library scan...")
|
||||
|
||||
try:
|
||||
success = self.plex_client.trigger_library_scan()
|
||||
|
||||
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()
|
||||
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 _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")
|
||||
|
||||
# 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 _reset_scan_state(self):
|
||||
"""Reset scan state after an error"""
|
||||
with self._lock:
|
||||
self._scan_in_progress = 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:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
logger.info("PlexScanManager shutdown - cancelled pending scan")
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -18,6 +18,7 @@ from core.spotify_client import SpotifyClient, Artist, Album
|
|||
from core.plex_client import PlexClient
|
||||
from core.soulseek_client import SoulseekClient, AlbumResult
|
||||
from core.matching_engine import MusicMatchingEngine
|
||||
from core.plex_scan_manager import PlexScanManager
|
||||
from database.music_database import get_database
|
||||
import asyncio
|
||||
|
||||
|
|
@ -1740,6 +1741,7 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
|||
self.album = album
|
||||
self.album_card = album_card
|
||||
self.parent_page = parent_page
|
||||
self.parent_artists_page = parent_page # Reference to artists page for scan manager
|
||||
self.downloads_page = downloads_page
|
||||
self.plex_client = plex_client
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
|
|
@ -2648,6 +2650,11 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
|||
self.process_finished.emit()
|
||||
except RuntimeError as e:
|
||||
print(f"⚠️ Modal object deleted during downloads complete signal: {e}")
|
||||
|
||||
# Request Plex library scan if we have successful downloads
|
||||
if self.successful_downloads > 0 and hasattr(self, 'parent_artists_page') and self.parent_artists_page.scan_manager:
|
||||
album_name = getattr(self.album, 'name', 'Unknown Album')
|
||||
self.parent_artists_page.scan_manager.request_scan(f"Album download completed: {album_name} ({self.successful_downloads} tracks)")
|
||||
|
||||
# Determine the final message based on success or failure
|
||||
if self.permanently_failed_tracks:
|
||||
|
|
@ -2815,9 +2822,13 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
|||
|
||||
def on_manual_match_resolved(self, resolved_track_info):
|
||||
"""Handle a track being successfully resolved by the ManualMatchModal"""
|
||||
print(f"🔧 Manual match resolved (Artists) - download_index: {resolved_track_info.get('download_index')}, table_index: {resolved_track_info.get('table_index')}")
|
||||
original_failed_track = next((t for t in self.permanently_failed_tracks if t['download_index'] == resolved_track_info['download_index']), None)
|
||||
if original_failed_track:
|
||||
self.permanently_failed_tracks.remove(original_failed_track)
|
||||
print(f"✅ Removed track from permanently_failed_tracks (Artists) - remaining: {len(self.permanently_failed_tracks)}")
|
||||
else:
|
||||
print("⚠️ Could not find original failed track to remove (Artists)")
|
||||
self.update_failed_matches_button()
|
||||
|
||||
class ArtistsPage(QWidget):
|
||||
|
|
@ -2850,6 +2861,9 @@ class ArtistsPage(QWidget):
|
|||
self.download_status_timer.timeout.connect(self.poll_album_download_statuses)
|
||||
self.download_status_timer.start(2000) # Poll every 2 seconds (consistent with sync.py)
|
||||
self.download_status_pool = QThreadPool()
|
||||
|
||||
# Initialize Plex scan manager (will be set when clients are connected)
|
||||
self.scan_manager = None
|
||||
self.download_status_pool.setMaxThreadCount(1) # One worker at a time to avoid conflicts
|
||||
self._is_status_update_running = False
|
||||
|
||||
|
|
@ -2878,6 +2892,11 @@ class ArtistsPage(QWidget):
|
|||
self.soulseek_client.download_path = download_path
|
||||
print(f"✅ Set soulseek_client download path for ArtistsPage to: {download_path}")
|
||||
# --- END FIX ---
|
||||
|
||||
# Initialize Plex scan manager now that clients are available
|
||||
if self.plex_client:
|
||||
self.scan_manager = PlexScanManager(self.plex_client, delay_seconds=60)
|
||||
print("✅ PlexScanManager initialized for ArtistsPage")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize clients: {e}")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import asyncio
|
|||
from core.matching_engine import MusicMatchingEngine
|
||||
from ui.components.toast_manager import ToastType
|
||||
from database.music_database import get_database
|
||||
from core.plex_scan_manager import PlexScanManager
|
||||
|
||||
# Define constants for storage
|
||||
STORAGE_DIR = "storage"
|
||||
|
|
@ -1984,6 +1985,11 @@ class SyncPage(QWidget):
|
|||
self.thread_pool = QThreadPool()
|
||||
self.thread_pool.setMaxThreadCount(3) # Limit concurrent Spotify API calls
|
||||
|
||||
# Initialize Plex scan manager
|
||||
self.scan_manager = None
|
||||
if self.plex_client:
|
||||
self.scan_manager = PlexScanManager(self.plex_client, delay_seconds=60)
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
# Don't auto-load on startup, but do auto-load when page becomes visible
|
||||
|
|
@ -3564,6 +3570,7 @@ class DownloadMissingTracksModal(QDialog):
|
|||
self.playlist = playlist
|
||||
self.playlist_item = playlist_item
|
||||
self.parent_page = parent_page
|
||||
self.parent_sync_page = parent_page # Reference to sync page for scan manager
|
||||
self.downloads_page = downloads_page
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
# State tracking
|
||||
|
|
@ -4163,6 +4170,7 @@ class DownloadMissingTracksModal(QDialog):
|
|||
|
||||
# Update UI to show the new download has been queued
|
||||
spotify_based_result = self.create_spotify_based_search_result_from_validation(slskd_result, spotify_metadata)
|
||||
print(f"🔧 Updating table at index {table_index} to '... Queued' for manual retry")
|
||||
self.track_table.setItem(table_index, 4, QTableWidgetItem("... Queued"))
|
||||
|
||||
# Start the actual download process
|
||||
|
|
@ -4380,6 +4388,7 @@ class DownloadMissingTracksModal(QDialog):
|
|||
|
||||
track_info['completed'] = True
|
||||
if success:
|
||||
print(f"🔧 Track {download_index} completed successfully - updating table index {track_info['table_index']} to '✅ Downloaded'")
|
||||
self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("✅ Downloaded"))
|
||||
self.downloaded_tracks_count += 1
|
||||
# --- FIX ---
|
||||
|
|
@ -4387,6 +4396,7 @@ class DownloadMissingTracksModal(QDialog):
|
|||
self.downloaded_count_label.setText(str(self.downloaded_tracks_count))
|
||||
self.successful_downloads += 1
|
||||
else:
|
||||
print(f"🔧 Track {download_index} failed - updating table index {track_info['table_index']} to '❌ Failed'")
|
||||
self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("❌ Failed"))
|
||||
self.failed_downloads += 1
|
||||
if track_info not in self.permanently_failed_tracks:
|
||||
|
|
@ -4421,9 +4431,13 @@ class DownloadMissingTracksModal(QDialog):
|
|||
|
||||
def on_manual_match_resolved(self, resolved_track_info):
|
||||
"""Handles a track being successfully resolved by the ManualMatchModal."""
|
||||
print(f"🔧 Manual match resolved - download_index: {resolved_track_info.get('download_index')}, table_index: {resolved_track_info.get('table_index')}")
|
||||
original_failed_track = next((t for t in self.permanently_failed_tracks if t['download_index'] == resolved_track_info['download_index']), None)
|
||||
if original_failed_track:
|
||||
self.permanently_failed_tracks.remove(original_failed_track)
|
||||
print(f"✅ Removed track from permanently_failed_tracks - remaining: {len(self.permanently_failed_tracks)}")
|
||||
else:
|
||||
print("⚠️ Could not find original failed track to remove")
|
||||
self.update_failed_matches_button()
|
||||
|
||||
def find_track_index_in_playlist(self, spotify_track):
|
||||
|
|
@ -4441,6 +4455,10 @@ class DownloadMissingTracksModal(QDialog):
|
|||
|
||||
# The process_finished signal is still emitted to unlock the main UI.
|
||||
self.process_finished.emit()
|
||||
|
||||
# Request Plex library scan if we have successful downloads
|
||||
if self.successful_downloads > 0 and hasattr(self, 'parent_sync_page') and self.parent_sync_page.scan_manager:
|
||||
self.parent_sync_page.scan_manager.request_scan(f"Playlist download completed ({self.successful_downloads} tracks)")
|
||||
|
||||
# Determine the final message based on success or failure.
|
||||
if self.permanently_failed_tracks:
|
||||
|
|
|
|||
Loading…
Reference in a new issue