bug fixes

This commit is contained in:
Broque Thomas 2025-08-07 14:53:16 -07:00
parent 906d0c23b7
commit 73679f5709
8 changed files with 203 additions and 0 deletions

Binary file not shown.

View file

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

View file

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

View file

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