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
|
import re
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
from config.settings import config_manager
|
from config.settings import config_manager
|
||||||
|
import threading
|
||||||
|
|
||||||
logger = get_logger("plex_client")
|
logger = get_logger("plex_client")
|
||||||
|
|
||||||
|
|
@ -630,6 +631,20 @@ class PlexClient:
|
||||||
logger.error(f"Error updating track metadata: {e}")
|
logger.error(f"Error updating track metadata: {e}")
|
||||||
return False
|
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]]:
|
def search_albums(self, album_name: str = "", artist_name: str = "", limit: int = 20) -> List[Dict[str, Any]]:
|
||||||
"""Search for albums in Plex library"""
|
"""Search for albums in Plex library"""
|
||||||
if not self.ensure_connection() or not self.music_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.plex_client import PlexClient
|
||||||
from core.soulseek_client import SoulseekClient, AlbumResult
|
from core.soulseek_client import SoulseekClient, AlbumResult
|
||||||
from core.matching_engine import MusicMatchingEngine
|
from core.matching_engine import MusicMatchingEngine
|
||||||
|
from core.plex_scan_manager import PlexScanManager
|
||||||
from database.music_database import get_database
|
from database.music_database import get_database
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
|
@ -1740,6 +1741,7 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
||||||
self.album = album
|
self.album = album
|
||||||
self.album_card = album_card
|
self.album_card = album_card
|
||||||
self.parent_page = parent_page
|
self.parent_page = parent_page
|
||||||
|
self.parent_artists_page = parent_page # Reference to artists page for scan manager
|
||||||
self.downloads_page = downloads_page
|
self.downloads_page = downloads_page
|
||||||
self.plex_client = plex_client
|
self.plex_client = plex_client
|
||||||
self.matching_engine = MusicMatchingEngine()
|
self.matching_engine = MusicMatchingEngine()
|
||||||
|
|
@ -2648,6 +2650,11 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
||||||
self.process_finished.emit()
|
self.process_finished.emit()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
print(f"⚠️ Modal object deleted during downloads complete signal: {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
|
# Determine the final message based on success or failure
|
||||||
if self.permanently_failed_tracks:
|
if self.permanently_failed_tracks:
|
||||||
|
|
@ -2815,9 +2822,13 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
||||||
|
|
||||||
def on_manual_match_resolved(self, resolved_track_info):
|
def on_manual_match_resolved(self, resolved_track_info):
|
||||||
"""Handle a track being successfully resolved by the ManualMatchModal"""
|
"""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)
|
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:
|
if original_failed_track:
|
||||||
self.permanently_failed_tracks.remove(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()
|
self.update_failed_matches_button()
|
||||||
|
|
||||||
class ArtistsPage(QWidget):
|
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.timeout.connect(self.poll_album_download_statuses)
|
||||||
self.download_status_timer.start(2000) # Poll every 2 seconds (consistent with sync.py)
|
self.download_status_timer.start(2000) # Poll every 2 seconds (consistent with sync.py)
|
||||||
self.download_status_pool = QThreadPool()
|
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.download_status_pool.setMaxThreadCount(1) # One worker at a time to avoid conflicts
|
||||||
self._is_status_update_running = False
|
self._is_status_update_running = False
|
||||||
|
|
||||||
|
|
@ -2878,6 +2892,11 @@ class ArtistsPage(QWidget):
|
||||||
self.soulseek_client.download_path = download_path
|
self.soulseek_client.download_path = download_path
|
||||||
print(f"✅ Set soulseek_client download path for ArtistsPage to: {download_path}")
|
print(f"✅ Set soulseek_client download path for ArtistsPage to: {download_path}")
|
||||||
# --- END FIX ---
|
# --- 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:
|
except Exception as e:
|
||||||
print(f"Failed to initialize clients: {e}")
|
print(f"Failed to initialize clients: {e}")
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import asyncio
|
||||||
from core.matching_engine import MusicMatchingEngine
|
from core.matching_engine import MusicMatchingEngine
|
||||||
from ui.components.toast_manager import ToastType
|
from ui.components.toast_manager import ToastType
|
||||||
from database.music_database import get_database
|
from database.music_database import get_database
|
||||||
|
from core.plex_scan_manager import PlexScanManager
|
||||||
|
|
||||||
# Define constants for storage
|
# Define constants for storage
|
||||||
STORAGE_DIR = "storage"
|
STORAGE_DIR = "storage"
|
||||||
|
|
@ -1984,6 +1985,11 @@ class SyncPage(QWidget):
|
||||||
self.thread_pool = QThreadPool()
|
self.thread_pool = QThreadPool()
|
||||||
self.thread_pool.setMaxThreadCount(3) # Limit concurrent Spotify API calls
|
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()
|
self.setup_ui()
|
||||||
|
|
||||||
# Don't auto-load on startup, but do auto-load when page becomes visible
|
# 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 = playlist
|
||||||
self.playlist_item = playlist_item
|
self.playlist_item = playlist_item
|
||||||
self.parent_page = parent_page
|
self.parent_page = parent_page
|
||||||
|
self.parent_sync_page = parent_page # Reference to sync page for scan manager
|
||||||
self.downloads_page = downloads_page
|
self.downloads_page = downloads_page
|
||||||
self.matching_engine = MusicMatchingEngine()
|
self.matching_engine = MusicMatchingEngine()
|
||||||
# State tracking
|
# State tracking
|
||||||
|
|
@ -4163,6 +4170,7 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
|
|
||||||
# Update UI to show the new download has been queued
|
# 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)
|
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"))
|
self.track_table.setItem(table_index, 4, QTableWidgetItem("... Queued"))
|
||||||
|
|
||||||
# Start the actual download process
|
# Start the actual download process
|
||||||
|
|
@ -4380,6 +4388,7 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
|
|
||||||
track_info['completed'] = True
|
track_info['completed'] = True
|
||||||
if success:
|
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.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("✅ Downloaded"))
|
||||||
self.downloaded_tracks_count += 1
|
self.downloaded_tracks_count += 1
|
||||||
# --- FIX ---
|
# --- FIX ---
|
||||||
|
|
@ -4387,6 +4396,7 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
self.downloaded_count_label.setText(str(self.downloaded_tracks_count))
|
self.downloaded_count_label.setText(str(self.downloaded_tracks_count))
|
||||||
self.successful_downloads += 1
|
self.successful_downloads += 1
|
||||||
else:
|
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.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("❌ Failed"))
|
||||||
self.failed_downloads += 1
|
self.failed_downloads += 1
|
||||||
if track_info not in self.permanently_failed_tracks:
|
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):
|
def on_manual_match_resolved(self, resolved_track_info):
|
||||||
"""Handles a track being successfully resolved by the ManualMatchModal."""
|
"""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)
|
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:
|
if original_failed_track:
|
||||||
self.permanently_failed_tracks.remove(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()
|
self.update_failed_matches_button()
|
||||||
|
|
||||||
def find_track_index_in_playlist(self, spotify_track):
|
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.
|
# The process_finished signal is still emitted to unlock the main UI.
|
||||||
self.process_finished.emit()
|
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.
|
# Determine the final message based on success or failure.
|
||||||
if self.permanently_failed_tracks:
|
if self.permanently_failed_tracks:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue