diff --git a/core/spotify_client.py b/core/spotify_client.py index 042b6ff5..41c4e5c9 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -13,7 +13,7 @@ logger = get_logger("spotify_client") # Global rate limiting variables _last_api_call_time = 0 _api_call_lock = threading.Lock() -MIN_API_INTERVAL = 0.1 # 100ms between API calls +MIN_API_INTERVAL = 0.2 # 200ms between API calls (more conservative to avoid bans) # Request queuing for burst handling import queue @@ -42,8 +42,12 @@ def rate_limited(func): except Exception as e: # Implement exponential backoff for API errors if "rate limit" in str(e).lower() or "429" in str(e): - logger.warning(f"Rate limit hit, backing off: {e}") - time.sleep(1.0) # Wait 1 second before retrying + logger.warning(f"Rate limit hit, implementing backoff: {e}") + # Use longer backoff to avoid getting banned + time.sleep(3.0) # Wait 3 seconds before retrying + elif "503" in str(e) or "502" in str(e): + logger.warning(f"Spotify service error, backing off: {e}") + time.sleep(2.0) # Wait 2 seconds for service errors raise e return wrapper diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index 9039be01..3a0a5767 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -8,6 +8,7 @@ from typing import List, Dict, Any, Optional from datetime import datetime, timezone from dataclasses import dataclass import re +import time from database.music_database import get_database, WatchlistArtist from core.spotify_client import SpotifyClient from core.wishlist_service import get_wishlist_service @@ -16,6 +17,11 @@ from utils.logging_config import get_logger logger = get_logger("watchlist_scanner") +# Rate limiting constants for watchlist operations +DELAY_BETWEEN_ARTISTS = 2.0 # 2 seconds between different artists +DELAY_BETWEEN_ALBUMS = 0.5 # 500ms between albums for same artist +DELAY_BETWEEN_API_BATCHES = 1.0 # 1 second between API batch operations + def clean_track_name_for_search(track_name): """ Intelligently cleans a track name for searching by removing noise while preserving important version information. @@ -135,12 +141,11 @@ class WatchlistScanner: else: logger.warning(f"❌ Failed to scan {artist.artist_name}: {result.error_message}") - # Rate limiting: Add a small delay between artists to avoid hitting Spotify limits - # Spotify allows ~100 requests per minute, so 1-2 second delay is safe + # Rate limiting: Add delay between artists to avoid hitting Spotify API limits + # This is critical to prevent getting banned for 6+ hours if i < len(watchlist_artists) - 1: # Don't delay after the last artist - import time - time.sleep(1.5) # 1.5 second delay between artists - logger.debug(f"Rate limiting: waited 1.5s before scanning next artist") + logger.debug(f"Rate limiting: waiting {DELAY_BETWEEN_ARTISTS}s before scanning next artist") + time.sleep(DELAY_BETWEEN_ARTISTS) except Exception as e: logger.error(f"Error scanning artist {artist.artist_name}: {e}") @@ -192,13 +197,20 @@ class WatchlistScanner: logger.info(f"Found {len(albums)} albums/singles to check for {watchlist_artist.artist_name}") + # Safety check: Limit number of albums to scan to prevent extremely long sessions + MAX_ALBUMS_PER_ARTIST = 50 # Reasonable limit to prevent API abuse + if len(albums) > MAX_ALBUMS_PER_ARTIST: + logger.warning(f"Artist {watchlist_artist.artist_name} has {len(albums)} albums, limiting to {MAX_ALBUMS_PER_ARTIST} most recent") + albums = albums[:MAX_ALBUMS_PER_ARTIST] # Most recent albums are first + # Check each album/single for missing tracks new_tracks_found = 0 tracks_added_to_wishlist = 0 - for album in albums: + for album_index, album in enumerate(albums): try: # Get full album data with tracks + logger.info(f"Checking album {album_index + 1}/{len(albums)}: {album.name}") album_data = self.spotify_client.get_album(album.id) if not album_data or 'tracks' not in album_data or not album_data['tracks'].get('items'): continue @@ -214,6 +226,12 @@ class WatchlistScanner: # Add to wishlist if self.add_track_to_wishlist(track, album_data, watchlist_artist): tracks_added_to_wishlist += 1 + + # Rate limiting: Add delay between albums to prevent API abuse + # This is especially important for artists with many albums + if album_index < len(albums) - 1: # Don't delay after the last album + logger.debug(f"Rate limiting: waiting {DELAY_BETWEEN_ALBUMS}s before next album") + time.sleep(DELAY_BETWEEN_ALBUMS) except Exception as e: logger.warning(f"Error checking album {album.name}: {e}") @@ -252,13 +270,17 @@ class WatchlistScanner: last_scan_timestamp: Only return releases after this date (for incremental scans) """ try: - # Get all artist albums (albums + singles) + # Get all artist albums (albums + singles) - this is rate limited in spotify_client + logger.debug(f"Fetching discography for artist {spotify_artist_id}") albums = self.spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single', limit=50) if not albums: logger.warning(f"No albums found for artist {spotify_artist_id}") return [] + # Add small delay after fetching artist discography to be extra safe + time.sleep(0.3) # 300ms breathing room + # Filter by release date if we have a last scan timestamp if last_scan_timestamp: filtered_albums = [] diff --git a/ui/components/watchlist_status_modal.py b/ui/components/watchlist_status_modal.py index 880d0392..d2e480a6 100644 --- a/ui/components/watchlist_status_modal.py +++ b/ui/components/watchlist_status_modal.py @@ -35,32 +35,69 @@ class WatchlistScanWorker(QThread): super().__init__() self.spotify_client = spotify_client self.should_stop = False + + # Progress state for reconnection + self.current_scan_state = { + 'total_artists': 0, + 'completed_artists': 0, + 'current_artist_name': '', + 'current_artist_total_singles_eps': 0, + 'current_artist_completed_singles_eps': 0, + 'current_artist_total_albums': 0, + 'current_artist_completed_albums': 0, + 'scan_active': False, + 'scan_completed': False + } def stop(self): """Stop the scanning process""" self.should_stop = True + self.current_scan_state['scan_active'] = False + + def get_current_progress(self): + """Get current progress state for reconnection""" + return self.current_scan_state.copy() def run(self): """Run the watchlist scan with detailed progress updates""" try: - self.scan_started.emit() - - # Get all watchlist artists + # Initialize progress state database = get_database() watchlist_artists = database.get_watchlist_artists() + self.current_scan_state.update({ + 'total_artists': len(watchlist_artists), + 'completed_artists': 0, + 'scan_active': True, + 'scan_completed': False + }) + + self.scan_started.emit() + scan_results = [] - for artist in watchlist_artists: + for i, artist in enumerate(watchlist_artists): if self.should_stop: break + # Update current artist progress state + self.current_scan_state.update({ + 'current_artist_name': artist.artist_name, + 'current_artist_total_singles_eps': 0, + 'current_artist_completed_singles_eps': 0, + 'current_artist_total_albums': 0, + 'current_artist_completed_albums': 0 + }) + self.artist_scan_started.emit(artist.artist_name) # Perform detailed scan with progress updates result = self._scan_artist_with_progress(artist, database) scan_results.append(result) + # Update completed artists count + self.current_scan_state['completed_artists'] = i + 1 + self.artist_scan_completed.emit( artist.artist_name, result.albums_checked, @@ -68,10 +105,20 @@ class WatchlistScanWorker(QThread): result.success ) + # Mark scan as completed + self.current_scan_state.update({ + 'scan_active': False, + 'scan_completed': True + }) + self.scan_completed.emit(scan_results) except Exception as e: logger.error(f"Error in watchlist scan worker: {e}") + self.current_scan_state.update({ + 'scan_active': False, + 'scan_completed': True + }) self.scan_completed.emit([]) def _scan_artist_with_progress(self, watchlist_artist, database): @@ -124,6 +171,12 @@ class WatchlistScanWorker(QThread): logger.warning(f"Error analyzing album {album.name} for totals: {e}") continue + # Update current artist totals in state + self.current_scan_state.update({ + 'current_artist_total_singles_eps': total_singles_eps_releases, + 'current_artist_total_albums': total_albums + }) + # Emit the discovered totals self.artist_totals_discovered.emit( watchlist_artist.artist_name, @@ -171,6 +224,12 @@ class WatchlistScanWorker(QThread): # Emit release completion signal self.release_completed.emit(watchlist_artist.artist_name, album.name, len(tracks)) + # Update progress state for this completed release + if len(tracks) >= 4: # Album + self.current_scan_state['current_artist_completed_albums'] += 1 + else: # Single/EP + self.current_scan_state['current_artist_completed_singles_eps'] += 1 + # Rate limiting: small delay between album processing to avoid hitting Spotify limits import time time.sleep(0.1) # 100ms delay between albums @@ -220,6 +279,9 @@ class WatchlistStatusModal(QDialog): # Keep track of whether this modal started the scan (vs background scan) self.is_manual_scan_owner = False + # Track when we're reconnecting to ongoing scan vs starting fresh + self.is_reconnecting_to_ongoing_scan = False + # Simple progress tracking self.total_artists = 0 self.completed_artists = 0 @@ -632,35 +694,43 @@ class WatchlistStatusModal(QDialog): return "✓", "#4caf50" # Default to up to date def create_artist_card(self, artist: WatchlistArtist) -> QFrame: - """Create a clean artist card widget""" + """Create a professional artist card widget""" card = QFrame() - card.setFixedHeight(36) # Reduced from 60 to 36 + card.setFixedHeight(48) # Increased height for better visual hierarchy card.setStyleSheet(""" QFrame { - background: rgba(40, 40, 40, 0.6); - border-radius: 8px; - border: 1px solid rgba(80, 80, 80, 0.3); + background: rgba(40, 40, 40, 0.8); + border-radius: 10px; + border: 1px solid rgba(80, 80, 80, 0.4); + } + QFrame:hover { + background: rgba(45, 45, 45, 0.9); + border: 1px solid rgba(100, 100, 100, 0.6); } """) layout = QHBoxLayout(card) - layout.setContentsMargins(12, 6, 12, 6) # Reduced vertical padding - layout.setSpacing(15) + layout.setContentsMargins(16, 8, 16, 8) + layout.setSpacing(16) - # Status indicator with icon (set based on scan history) + # Status indicator with icon status_icon, status_color = self.get_artist_status_icon(artist) status_label = QLabel(status_icon) - status_label.setFont(QFont("Arial", 12)) + status_label.setFont(QFont("Arial", 14)) status_label.setStyleSheet(f"color: {status_color}; border: none; background: transparent;") - status_label.setFixedWidth(20) + status_label.setFixedWidth(24) status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - # Artist name - name_label = QLabel(artist.artist_name) - name_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) - name_label.setStyleSheet("color: #ffffff; border: none; background: transparent;") + # Left side: Artist info + info_layout = QVBoxLayout() + info_layout.setSpacing(2) - # Last scan info (compact format) + # Artist name with label + artist_label = QLabel(f"Artist: {artist.artist_name}") + artist_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) + artist_label.setStyleSheet("color: #ffffff; border: none; background: transparent;") + + # Last scan info with professional formatting if artist.last_scan_timestamp: try: from datetime import datetime, timezone @@ -674,44 +744,48 @@ class WatchlistStatusModal(QDialog): time_diff = now - last_scan if time_diff.days > 0: - scan_info = f"{time_diff.days}d ago" + scan_time = f"{time_diff.days} day{'s' if time_diff.days > 1 else ''} ago" elif time_diff.seconds > 3600: hours = time_diff.seconds // 3600 - scan_info = f"{hours}h ago" + scan_time = f"{hours} hour{'s' if hours > 1 else ''} ago" else: minutes = max(1, time_diff.seconds // 60) - scan_info = f"{minutes}m ago" + scan_time = f"{minutes} minute{'s' if minutes > 1 else ''} ago" except Exception as e: - # Fallback to simple format if timezone calculation fails - scan_info = artist.last_scan_timestamp.strftime("%m-%d %H:%M") + # Fallback to formatted date + scan_time = last_scan.strftime("%m/%d/%Y at %I:%M %p") else: - scan_info = "Never" + scan_time = "never scanned" - scan_label = QLabel(scan_info) - scan_label.setFont(QFont("Arial", 10)) - scan_label.setStyleSheet("color: #b3b3b3; border: none; background: transparent;") - scan_label.setFixedWidth(60) - scan_label.setAlignment(Qt.AlignmentFlag.AlignRight) + sync_label = QLabel(f"Last Sync: {scan_time}") + sync_label.setFont(QFont("Arial", 10)) + sync_label.setStyleSheet("color: #b3b3b3; border: none; background: transparent;") - # Delete button - delete_button = QPushButton("×") - delete_button.setFixedSize(24, 24) + info_layout.addWidget(artist_label) + info_layout.addWidget(sync_label) + + # Delete button with modern styling + delete_button = QPushButton("✕") + delete_button.setFixedSize(28, 28) delete_button.setFont(QFont("Arial", 12, QFont.Weight.Bold)) delete_button.setStyleSheet(""" QPushButton { - background: rgba(244, 67, 54, 0.8); - color: white; - border: none; - border-radius: 12px; + background: rgba(244, 67, 54, 0.1); + color: #f44336; + border: 1px solid rgba(244, 67, 54, 0.3); + border-radius: 14px; font-weight: bold; } QPushButton:hover { - background: rgba(244, 67, 54, 1.0); + background: rgba(244, 67, 54, 0.8); + color: white; + border: 1px solid #f44336; } QPushButton:pressed { background: rgba(200, 50, 40, 1.0); } """) + delete_button.setToolTip(f"Remove {artist.artist_name} from watchlist") delete_button.clicked.connect(lambda: self.delete_artist(artist)) # Store references for updates @@ -719,9 +793,8 @@ class WatchlistStatusModal(QDialog): setattr(card, 'artist_id', artist.spotify_artist_id) layout.addWidget(status_label) - layout.addWidget(name_label) + layout.addLayout(info_layout) layout.addStretch() - layout.addWidget(scan_label) layout.addWidget(delete_button) return card @@ -815,24 +888,29 @@ class WatchlistStatusModal(QDialog): def on_scan_started(self): """Handle scan start""" - self.current_action_label.setText("Starting watchlist scan...") + # Only reset progress if this is a fresh scan, not a reconnection + if not self.is_reconnecting_to_ongoing_scan: + self.current_action_label.setText("Starting watchlist scan...") + + # Reset overall counters + self.total_artists = len(self.current_artists) + self.completed_artists = 0 + + # Reset current artist tracking + self.current_artist_name = "" + self.current_artist_total_singles_eps = 0 + self.current_artist_completed_singles_eps = 0 + self.current_artist_total_albums = 0 + self.current_artist_completed_albums = 0 + + # Reset progress bars + self.singles_progress_bar.setValue(0) + self.albums_progress_bar.setValue(0) + self.artists_progress_bar.setValue(0) + self.scan_summary_label.setText("Preparing to scan artists...") - # Reset overall counters - self.total_artists = len(self.current_artists) - self.completed_artists = 0 - - # Reset current artist tracking - self.current_artist_name = "" - self.current_artist_total_singles_eps = 0 - self.current_artist_completed_singles_eps = 0 - self.current_artist_total_albums = 0 - self.current_artist_completed_albums = 0 - - # Reset progress bars - self.singles_progress_bar.setValue(0) - self.albums_progress_bar.setValue(0) - self.artists_progress_bar.setValue(0) - self.scan_summary_label.setText("Preparing to scan artists...") + # Clear the reconnection flag after handling + self.is_reconnecting_to_ongoing_scan = False def on_artist_scan_started(self, artist_name: str): """Handle individual artist scan start""" @@ -949,9 +1027,12 @@ class WatchlistStatusModal(QDialog): self.scan_button.setText("Start Scan") self.scan_button.setEnabled(True) - # Clean up shared worker if this was a manual scan - if self.scan_worker == WatchlistStatusModal._shared_scan_worker: - WatchlistStatusModal._shared_scan_worker = None + # Keep shared worker around for a bit so other modals can see completed results + # Only clear it if this modal was the owner (manual scan starter) + if (self.scan_worker == WatchlistStatusModal._shared_scan_worker + and WatchlistStatusModal._scan_owner_modal == self): + # Keep the worker alive for 30 seconds to allow other modals to see results + QTimer.singleShot(30000, self._cleanup_shared_worker_delayed) WatchlistStatusModal._scan_owner_modal = None # Calculate summary @@ -1021,28 +1102,42 @@ class WatchlistStatusModal(QDialog): super().showEvent(event) self.load_watchlist_data() - # First check if there's an ongoing manual scan from this modal class - if (WatchlistStatusModal._shared_scan_worker - and WatchlistStatusModal._shared_scan_worker.isRunning()): + # First check if there's a manual scan worker (running or recently completed) + if WatchlistStatusModal._shared_scan_worker: - logger.info("Found ongoing manual watchlist scan - reconnecting to it") + logger.info("Found manual watchlist scan worker - reconnecting to it") # Reconnect to the shared manual scan worker self.scan_worker = WatchlistStatusModal._shared_scan_worker - self.scan_in_progress = True + + # Check if scan is still active + progress_state = self.scan_worker.get_current_progress() + self.scan_in_progress = progress_state.get('scan_active', False) if progress_state else False + self.is_reconnecting_to_ongoing_scan = True try: + # Restore progress state BEFORE connecting signals to prevent reset conflicts + self._restore_progress_state(progress_state) + + # Now connect to future signals self.scan_worker.scan_started.connect(self.on_scan_started) self.scan_worker.artist_scan_started.connect(self.on_artist_scan_started) + self.scan_worker.artist_totals_discovered.connect(self.on_artist_totals_discovered) self.scan_worker.album_scan_started.connect(self.on_album_scan_started) self.scan_worker.track_check_started.connect(self.on_track_check_started) + self.scan_worker.release_completed.connect(self.on_release_completed) self.scan_worker.artist_scan_completed.connect(self.on_artist_scan_completed) self.scan_worker.scan_completed.connect(self.on_scan_completed) - # Update UI to show scan in progress - self.current_action_label.setText("Manual scan in progress...") - self.scan_button.setText("Scanning...") - self.scan_button.setEnabled(False) + # Update UI to show reconnection status (will be overridden by _restore_progress_state if needed) + if self.scan_in_progress: + self.current_action_label.setText("Reconnected to manual scan...") + self.scan_button.setText("Scanning...") + self.scan_button.setEnabled(False) + else: + self.current_action_label.setText("Viewing completed manual scan results") + self.scan_button.setText("Start Scan") + self.scan_button.setEnabled(True) except Exception as e: logger.debug(f"Could not connect to manual scan signals (may already be connected): {e}") @@ -1064,24 +1159,37 @@ class WatchlistStatusModal(QDialog): # If we found the dashboard and there's an active background worker if (dashboard and hasattr(dashboard, 'background_watchlist_worker') and dashboard.background_watchlist_worker + and dashboard.background_watchlist_worker.isRunning() and hasattr(dashboard, 'auto_processing_watchlist') and dashboard.auto_processing_watchlist): logger.info("Found active background watchlist scan - connecting modal to live updates") - # Connect to the background worker's signals for live updates + # Set reconnection flag and restore progress before connecting signals + self.is_reconnecting_to_ongoing_scan = True + + # Restore progress state BEFORE connecting signals try: - dashboard.background_watchlist_worker.signals.scan_started.connect(self.on_scan_started) - dashboard.background_watchlist_worker.signals.artist_scan_started.connect(self.on_artist_scan_started) - dashboard.background_watchlist_worker.signals.album_scan_started.connect(self.on_album_scan_started) - dashboard.background_watchlist_worker.signals.track_check_started.connect(self.on_track_check_started) - dashboard.background_watchlist_worker.signals.artist_scan_completed.connect(self.on_artist_scan_completed) + progress_state = dashboard.background_watchlist_worker.get_current_progress() + self._restore_progress_state(progress_state) + except Exception as e: + logger.debug(f"Could not restore background scan progress: {e}") + + # Connect to the background worker's signals for live updates + # Now using the same WatchlistScanWorker signals (no .signals attribute needed) + try: + dashboard.background_watchlist_worker.scan_started.connect(self.on_scan_started) + dashboard.background_watchlist_worker.artist_scan_started.connect(self.on_artist_scan_started) + dashboard.background_watchlist_worker.artist_totals_discovered.connect(self.on_artist_totals_discovered) + dashboard.background_watchlist_worker.album_scan_started.connect(self.on_album_scan_started) + dashboard.background_watchlist_worker.track_check_started.connect(self.on_track_check_started) + dashboard.background_watchlist_worker.release_completed.connect(self.on_release_completed) + dashboard.background_watchlist_worker.artist_scan_completed.connect(self.on_artist_scan_completed) - # Update UI to show scan in progress - self.current_action_label.setText("Background scan in progress...") + # Update UI to show reconnection status + self.current_action_label.setText("Reconnected to background scan...") self.scan_button.setText("Background Scanning...") self.scan_button.setEnabled(False) - self.artists_progress_bar.setValue(0) # Will be updated by signals except Exception as e: logger.debug(f"Could not connect to background scan signals (may already be connected): {e}") @@ -1090,6 +1198,78 @@ class WatchlistStatusModal(QDialog): logger.debug(f"Error checking for background scan: {e}") # Not critical - just means we can't detect ongoing scans + @staticmethod + def _cleanup_shared_worker_delayed(): + """Clean up shared worker after delay to allow other modals to see results""" + try: + if WatchlistStatusModal._shared_scan_worker: + if not WatchlistStatusModal._shared_scan_worker.isRunning(): + WatchlistStatusModal._shared_scan_worker = None + logger.debug("Cleaned up completed shared scan worker") + except Exception as e: + logger.debug(f"Error cleaning up shared worker: {e}") + + def _restore_progress_state(self, progress_state): + """Restore progress bars and UI state from worker's current progress""" + if not progress_state: + return + + # Handle both active and completed scans + is_active = progress_state.get('scan_active', False) + is_completed = progress_state.get('scan_completed', False) + + if not is_active and not is_completed: + return + + try: + # Fully sync modal state with worker state + self.total_artists = progress_state.get('total_artists', 0) + self.completed_artists = progress_state.get('completed_artists', 0) + self.current_artist_name = progress_state.get('current_artist_name', '') + self.current_artist_total_singles_eps = progress_state.get('current_artist_total_singles_eps', 0) + self.current_artist_completed_singles_eps = progress_state.get('current_artist_completed_singles_eps', 0) + self.current_artist_total_albums = progress_state.get('current_artist_total_albums', 0) + self.current_artist_completed_albums = progress_state.get('current_artist_completed_albums', 0) + + # Update UI elements + if self.total_artists > 0: + overall_progress = int((self.completed_artists / self.total_artists) * 100) + self.artists_progress_bar.setValue(overall_progress) + + if self.current_artist_name: + self.current_action_label.setText(f"Scanning: {self.current_artist_name}") + + # Update current artist progress bars + if self.current_artist_total_singles_eps > 0: + singles_progress = int((self.current_artist_completed_singles_eps / self.current_artist_total_singles_eps) * 100) + self.singles_progress_bar.setValue(singles_progress) + + if self.current_artist_total_albums > 0: + albums_progress = int((self.current_artist_completed_albums / self.current_artist_total_albums) * 100) + self.albums_progress_bar.setValue(albums_progress) + + # Update scan summary and UI state based on scan status + if is_completed: + self.scan_summary_label.setText("Scan completed - viewing final results") + self.scan_button.setText("Start Scan") + self.scan_button.setEnabled(True) + # Set progress bars to 100% for completed scans + self.artists_progress_bar.setValue(100) + if self.current_artist_total_singles_eps > 0: + self.singles_progress_bar.setValue(100) + if self.current_artist_total_albums > 0: + self.albums_progress_bar.setValue(100) + elif is_active: + remaining_artists = self.total_artists - self.completed_artists + self.scan_summary_label.setText(f"Reconnected to ongoing scan - {remaining_artists} artists remaining") + self.scan_button.setText("Scanning...") + self.scan_button.setEnabled(False) + + logger.info(f"Restored progress state: {self.completed_artists}/{self.total_artists} artists, current: {self.current_artist_name}") + + except Exception as e: + logger.error(f"Error restoring progress state: {e}") + def closeEvent(self, event): """Handle modal close""" # Only stop the scan if this modal owns it and it's not a shared manual scan @@ -1111,11 +1291,13 @@ class WatchlistStatusModal(QDialog): if (parent_widget and hasattr(parent_widget, 'background_watchlist_worker') and parent_widget.background_watchlist_worker): try: - parent_widget.background_watchlist_worker.signals.scan_started.disconnect(self.on_scan_started) - parent_widget.background_watchlist_worker.signals.artist_scan_started.disconnect(self.on_artist_scan_started) - parent_widget.background_watchlist_worker.signals.album_scan_started.disconnect(self.on_album_scan_started) - parent_widget.background_watchlist_worker.signals.track_check_started.disconnect(self.on_track_check_started) - parent_widget.background_watchlist_worker.signals.artist_scan_completed.disconnect(self.on_artist_scan_completed) + parent_widget.background_watchlist_worker.scan_started.disconnect(self.on_scan_started) + parent_widget.background_watchlist_worker.artist_scan_started.disconnect(self.on_artist_scan_started) + parent_widget.background_watchlist_worker.artist_totals_discovered.disconnect(self.on_artist_totals_discovered) + parent_widget.background_watchlist_worker.album_scan_started.disconnect(self.on_album_scan_started) + parent_widget.background_watchlist_worker.track_check_started.disconnect(self.on_track_check_started) + parent_widget.background_watchlist_worker.release_completed.disconnect(self.on_release_completed) + parent_widget.background_watchlist_worker.artist_scan_completed.disconnect(self.on_artist_scan_completed) except: pass # Ignore if signals weren't connected except: diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index 3efddb17..00d5b0c3 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -3947,22 +3947,23 @@ class DashboardPage(QWidget): # Emit signal to any open modal self.watchlist_scan_started.emit() - # Start background watchlist scan - self.background_watchlist_worker = AutoWatchlistScanWorker(spotify_client) - self.background_watchlist_worker.signals.scan_complete.connect(self.on_auto_watchlist_scan_complete) - self.background_watchlist_worker.signals.scan_error.connect(self.on_auto_watchlist_scan_error) + # Start background watchlist scan using the same worker as manual scans for consistency + from ui.components.watchlist_status_modal import WatchlistScanWorker + self.background_watchlist_worker = WatchlistScanWorker(spotify_client) + self.background_watchlist_worker.scan_completed.connect(self.on_auto_watchlist_scan_complete_unified) # Connect detailed progress signals to modal if it's open if hasattr(self, 'watchlist_status_modal') and self.watchlist_status_modal and self.watchlist_status_modal.isVisible(): - self.background_watchlist_worker.signals.scan_started.connect(self.watchlist_status_modal.on_scan_started) - self.background_watchlist_worker.signals.artist_scan_started.connect(self.watchlist_status_modal.on_artist_scan_started) - self.background_watchlist_worker.signals.artist_totals_discovered.connect(self.watchlist_status_modal.on_artist_totals_discovered) - self.background_watchlist_worker.signals.album_scan_started.connect(self.watchlist_status_modal.on_album_scan_started) - self.background_watchlist_worker.signals.track_check_started.connect(self.watchlist_status_modal.on_track_check_started) - self.background_watchlist_worker.signals.release_completed.connect(self.watchlist_status_modal.on_release_completed) - self.background_watchlist_worker.signals.artist_scan_completed.connect(self.watchlist_status_modal.on_artist_scan_completed) + self.background_watchlist_worker.scan_started.connect(self.watchlist_status_modal.on_scan_started) + self.background_watchlist_worker.artist_scan_started.connect(self.watchlist_status_modal.on_artist_scan_started) + self.background_watchlist_worker.artist_totals_discovered.connect(self.watchlist_status_modal.on_artist_totals_discovered) + self.background_watchlist_worker.album_scan_started.connect(self.watchlist_status_modal.on_album_scan_started) + self.background_watchlist_worker.track_check_started.connect(self.watchlist_status_modal.on_track_check_started) + self.background_watchlist_worker.release_completed.connect(self.watchlist_status_modal.on_release_completed) + self.background_watchlist_worker.artist_scan_completed.connect(self.watchlist_status_modal.on_artist_scan_completed) - QThreadPool.globalInstance().start(self.background_watchlist_worker) + # Start the thread (not QThreadPool since this is now a QThread) + self.background_watchlist_worker.start() except Exception as e: logger.error(f"Error starting automatic watchlist scanning: {e}") @@ -3970,11 +3971,17 @@ class DashboardPage(QWidget): # Reschedule on error self.watchlist_scan_timer.start(600000) # 10 minutes - def on_auto_watchlist_scan_complete(self, total_artists: int, total_new_tracks: int, total_added_to_wishlist: int): - """Handle completion of automatic watchlist scanning""" + def on_auto_watchlist_scan_complete_unified(self, scan_results): + """Handle completion of automatic watchlist scanning using unified WatchlistScanWorker""" try: self.auto_processing_watchlist = False + # Calculate summary from scan results (same as modal does) + successful_scans = [r for r in scan_results if r.success] + total_artists = len(scan_results) + total_new_tracks = sum(r.new_tracks_found for r in successful_scans) + total_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans) + # Clear background worker reference if hasattr(self, 'background_watchlist_worker'): self.background_watchlist_worker = None @@ -4000,217 +4007,11 @@ class DashboardPage(QWidget): except Exception as e: logger.error(f"Error handling automatic watchlist scan completion: {e}") - - def on_auto_watchlist_scan_error(self, error_message: str): - """Handle error in automatic watchlist scanning""" - try: - self.auto_processing_watchlist = False - - # Clear background worker reference - if hasattr(self, 'background_watchlist_worker'): - self.background_watchlist_worker = None - logger.error(f"Automatic watchlist scanning failed: {error_message}") - - # Schedule next watchlist scan in 60 minutes even after error + # Ensure we reschedule even on error if hasattr(self, 'watchlist_scan_timer') and self.watchlist_scan_timer: - logger.info("Scheduling next automatic watchlist scan in 60 minutes (after error)") self.watchlist_scan_timer.start(600000) # 10 minutes - - except Exception as e: - logger.error(f"Error handling automatic watchlist scan error: {e}") -class AutoWatchlistScanWorker(QRunnable): - """Background worker for automatic watchlist scanning with detailed progress updates""" - - class Signals(QObject): - # Summary signals for dashboard - scan_complete = pyqtSignal(int, int, int) # total_artists, total_new_tracks, total_added_to_wishlist - scan_error = pyqtSignal(str) # error_message - - # Detailed progress signals for modal (same as WatchlistScanWorker) - scan_started = pyqtSignal() - artist_scan_started = pyqtSignal(str) # artist_name - artist_totals_discovered = pyqtSignal(str, int, int) # artist_name, total_singles_eps_releases, total_albums - album_scan_started = pyqtSignal(str, str, int) # artist_name, album_name, total_tracks - track_check_started = pyqtSignal(str, str, str) # artist_name, album_name, track_name - release_completed = pyqtSignal(str, str, int) # artist_name, album_name, total_tracks - artist_scan_completed = pyqtSignal(str, int, int, bool) # artist_name, albums_checked, new_tracks, success - - def __init__(self, spotify_client): - super().__init__() - self.spotify_client = spotify_client - self.signals = self.Signals() - self.should_stop = False - - def stop(self): - """Stop the scanning process""" - self.should_stop = True - - def run(self): - """Run the watchlist scan with detailed progress updates""" - try: - logger.info("Starting background watchlist scan...") - self.signals.scan_started.emit() - - # Get all watchlist artists - from database.music_database import get_database - database = get_database() - watchlist_artists = database.get_watchlist_artists() - - scan_results = [] - - for artist in watchlist_artists: - if self.should_stop: - break - - self.signals.artist_scan_started.emit(artist.artist_name) - - # Perform detailed scan with progress updates - result = self._scan_artist_with_progress(artist, database) - scan_results.append(result) - - self.signals.artist_scan_completed.emit( - artist.artist_name, - result.albums_checked, - result.new_tracks_found, - result.success - ) - - # Calculate totals - successful_scans = [r for r in scan_results if r.success] - total_artists = len(successful_scans) - total_new_tracks = sum(r.new_tracks_found for r in successful_scans) - total_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans) - - self.signals.scan_complete.emit(total_artists, total_new_tracks, total_added_to_wishlist) - - except Exception as e: - logger.error(f"Error in background watchlist scan: {e}") - self.signals.scan_error.emit(str(e)) - - def _scan_artist_with_progress(self, watchlist_artist, database): - """Scan artist with detailed progress emissions (same as WatchlistScanWorker)""" - try: - # Get watchlist scanner - from core.watchlist_scanner import get_watchlist_scanner, ScanResult - scanner = get_watchlist_scanner(self.spotify_client) - - # Get artist discography - albums = scanner.get_artist_discography( - watchlist_artist.spotify_artist_id, - watchlist_artist.last_scan_timestamp - ) - - if albums is None: - return ScanResult( - artist_name=watchlist_artist.artist_name, - spotify_artist_id=watchlist_artist.spotify_artist_id, - albums_checked=0, - new_tracks_found=0, - tracks_added_to_wishlist=0, - success=False, - error_message="Failed to get artist discography from Spotify" - ) - - # Analyze the albums list to get total counts upfront - total_singles_eps_releases = 0 - total_albums = 0 - - for album in albums: - try: - # Get full album data to count tracks - album_data = self.spotify_client.get_album(album.id) - if not album_data or 'tracks' not in album_data: - continue - - track_count = len(album_data['tracks'].get('items', [])) - - # Categorize based on track count - COUNT RELEASES not tracks - if track_count >= 4: - total_albums += 1 - else: - total_singles_eps_releases += 1 # Count the release, not the tracks - - except Exception as e: - logger.warning(f"Error analyzing album {album.name} for totals: {e}") - continue - - # Emit the discovered totals - self.signals.artist_totals_discovered.emit( - watchlist_artist.artist_name, - total_singles_eps_releases, - total_albums - ) - - new_tracks_found = 0 - tracks_added_to_wishlist = 0 - - for album in albums: - if self.should_stop: - break - - try: - # Get full album data with tracks first - album_data = self.spotify_client.get_album(album.id) - if not album_data or 'tracks' not in album_data or not album_data['tracks'].get('items'): - continue - - tracks = album_data['tracks']['items'] - - # Emit album progress with track count - self.signals.album_scan_started.emit(watchlist_artist.artist_name, album.name, len(tracks)) - - # Check each track - for track in tracks: - if self.should_stop: - break - - # Emit track check progress - self.signals.track_check_started.emit( - watchlist_artist.artist_name, - album_data.get('name', 'Unknown'), - track.get('name', 'Unknown') - ) - - if scanner.is_track_missing_from_library(track): - new_tracks_found += 1 - - # Add to wishlist - if scanner.add_track_to_wishlist(track, album_data, watchlist_artist): - tracks_added_to_wishlist += 1 - - # Emit release completion signal - self.signals.release_completed.emit(watchlist_artist.artist_name, album.name, len(tracks)) - - except Exception as e: - logger.warning(f"Error checking album {album.name}: {e}") - continue - - # Update last scan timestamp - scanner.update_artist_scan_timestamp(watchlist_artist.spotify_artist_id) - - return ScanResult( - artist_name=watchlist_artist.artist_name, - spotify_artist_id=watchlist_artist.spotify_artist_id, - albums_checked=len(albums), - new_tracks_found=new_tracks_found, - tracks_added_to_wishlist=tracks_added_to_wishlist, - success=True - ) - - except Exception as e: - logger.error(f"Error scanning artist {watchlist_artist.artist_name}: {e}") - return ScanResult( - artist_name=watchlist_artist.artist_name, - spotify_artist_id=watchlist_artist.spotify_artist_id, - albums_checked=0, - new_tracks_found=0, - tracks_added_to_wishlist=0, - success=False, - error_message=str(e) - ) - class AutoWishlistProcessorWorker(QRunnable): """Background worker for automatic wishlist processing""" diff --git a/ui/pages/settings.py b/ui/pages/settings.py index 2b76accd..b0e47f8c 100644 --- a/ui/pages/settings.py +++ b/ui/pages/settings.py @@ -1,6 +1,6 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QPushButton, QLineEdit, QComboBox, - QCheckBox, QSpinBox, QTextEdit, QGroupBox, QFormLayout, QMessageBox, QSizePolicy) + QCheckBox, QSpinBox, QTextEdit, QGroupBox, QFormLayout, QMessageBox, QSizePolicy, QScrollArea) from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QFont from config.settings import config_manager @@ -679,28 +679,61 @@ class SettingsPage(QWidget): } """) + # Main container layout main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(20, 16, 20, 20) - main_layout.setSpacing(16) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # Create scroll area + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + scroll_area.setStyleSheet(""" + QScrollArea { + background: #191414; + border: none; + } + QScrollBar:vertical { + background: #282828; + width: 12px; + border-radius: 6px; + } + QScrollBar::handle:vertical { + background: #535353; + min-height: 20px; + border-radius: 6px; + } + QScrollBar::handle:vertical:hover { + background: #727272; + } + """) + + # Create scrollable content widget + scroll_content = QWidget() + scroll_content.setStyleSheet("background: #191414;") + content_layout = QVBoxLayout(scroll_content) + content_layout.setContentsMargins(20, 16, 20, 20) + content_layout.setSpacing(16) # Header header = self.create_header() - main_layout.addWidget(header) + content_layout.addWidget(header) # Settings content - content_layout = QHBoxLayout() - content_layout.setSpacing(24) + settings_layout = QHBoxLayout() + settings_layout.setSpacing(24) # Left column left_column = self.create_left_column() - content_layout.addWidget(left_column) + settings_layout.addWidget(left_column) # Right column right_column = self.create_right_column() - content_layout.addWidget(right_column) + settings_layout.addWidget(right_column) - main_layout.addLayout(content_layout) - main_layout.addStretch() + content_layout.addLayout(settings_layout) + content_layout.addStretch() # Save button self.save_btn = QPushButton("💾 Save Settings") @@ -720,7 +753,11 @@ class SettingsPage(QWidget): } """) - main_layout.addWidget(self.save_btn) + content_layout.addWidget(self.save_btn) + + # Set scroll area content and add to main layout + scroll_area.setWidget(scroll_content) + main_layout.addWidget(scroll_area) def load_config_values(self): """Load current configuration values into form inputs""" @@ -957,7 +994,7 @@ class SettingsPage(QWidget): # Status label self.status_label = QLabel("Checking local machine...") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.status_label.setStyleSheet("color: #b3b3b3; font-size: 12px;") + self.status_label.setStyleSheet("color: #ffffff; font-size: 12px; background: transparent;") layout.addWidget(self.status_label) # Animated loading bar container @@ -1061,7 +1098,7 @@ class SettingsPage(QWidget): # Status label self.status_label = QLabel("Checking local machine...") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.status_label.setStyleSheet("color: #b3b3b3; font-size: 12px;") + self.status_label.setStyleSheet("color: #ffffff; font-size: 12px; background: transparent;") layout.addWidget(self.status_label) # Animated loading bar container @@ -1301,7 +1338,7 @@ class SettingsPage(QWidget): # Info text info_label = QLabel("URL automatically filled in settings above.") - info_label.setStyleSheet("color: #b3b3b3; font-size: 9px; font-style: italic;") + info_label.setStyleSheet("color: #ffffff; font-size: 9px; font-style: italic; background: transparent;") info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(info_label) @@ -1412,7 +1449,7 @@ class SettingsPage(QWidget): # Info text info_label = QLabel("URL automatically filled in settings above.") - info_label.setStyleSheet("color: #b3b3b3; font-size: 9px; font-style: italic;") + info_label.setStyleSheet("color: #ffffff; font-size: 9px; font-style: italic; background: transparent;") info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(info_label) @@ -1494,12 +1531,12 @@ class SettingsPage(QWidget): # Title title_label = QLabel("Settings") title_label.setFont(QFont("Arial", 28, QFont.Weight.Bold)) - title_label.setStyleSheet("color: #ffffff;") + title_label.setStyleSheet("color: #ffffff; background: transparent;") # Subtitle subtitle_label = QLabel("Configure your music sync and download preferences") subtitle_label.setFont(QFont("Arial", 14)) - subtitle_label.setStyleSheet("color: #b3b3b3;") + subtitle_label.setStyleSheet("color: #ffffff; background: transparent;") layout.addWidget(title_label) layout.addWidget(subtitle_label) @@ -1519,6 +1556,14 @@ class SettingsPage(QWidget): # Spotify settings spotify_frame = QFrame() + spotify_frame.setStyleSheet(""" + QFrame { + background: #333333; + border: 1px solid #444444; + border-radius: 8px; + padding: 8px; + } + """) spotify_layout = QVBoxLayout(spotify_frame) spotify_layout.setSpacing(8) @@ -1529,7 +1574,7 @@ class SettingsPage(QWidget): # Client ID client_id_label = QLabel("Client ID:") - client_id_label.setStyleSheet("color: #ffffff; font-size: 11px;") + client_id_label.setStyleSheet(self.get_label_style(11)) spotify_layout.addWidget(client_id_label) self.client_id_input = QLineEdit() @@ -1540,7 +1585,7 @@ class SettingsPage(QWidget): # Client Secret client_secret_label = QLabel("Client Secret:") - client_secret_label.setStyleSheet("color: #ffffff; font-size: 11px;") + client_secret_label.setStyleSheet(self.get_label_style(11)) spotify_layout.addWidget(client_secret_label) self.client_secret_input = QLineEdit() @@ -1552,7 +1597,7 @@ class SettingsPage(QWidget): # Callback URL info callback_info_label = QLabel("Required Redirect URI:") - callback_info_label.setStyleSheet("color: #b3b3b3; font-size: 11px; margin-top: 8px;") + callback_info_label.setStyleSheet("color: #ffffff; font-size: 11px; margin-top: 8px; background: transparent;") spotify_layout.addWidget(callback_info_label) callback_url_label = QLabel("http://127.0.0.1:8888/callback") @@ -1571,12 +1616,20 @@ class SettingsPage(QWidget): # Helper text helper_text = QLabel("Add this URL to your Spotify app's 'Redirect URIs' in the Spotify Developer Dashboard") - helper_text.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;") + helper_text.setStyleSheet("color: #ffffff; font-size: 10px; font-style: italic; background: transparent;") helper_text.setWordWrap(True) spotify_layout.addWidget(helper_text) # Plex settings plex_frame = QFrame() + plex_frame.setStyleSheet(""" + QFrame { + background: #333333; + border: 1px solid #444444; + border-radius: 8px; + padding: 8px; + } + """) plex_layout = QVBoxLayout(plex_frame) plex_layout.setSpacing(8) @@ -1587,7 +1640,7 @@ class SettingsPage(QWidget): # Server URL plex_url_label = QLabel("Server URL:") - plex_url_label.setStyleSheet("color: #ffffff; font-size: 11px;") + plex_url_label.setStyleSheet(self.get_label_style(11)) plex_layout.addWidget(plex_url_label) plex_url_input_layout = QHBoxLayout() @@ -1607,7 +1660,7 @@ class SettingsPage(QWidget): # Token plex_token_label = QLabel("Token:") - plex_token_label.setStyleSheet("color: #ffffff; font-size: 11px;") + plex_token_label.setStyleSheet(self.get_label_style(11)) plex_layout.addWidget(plex_token_label) self.plex_token_input = QLineEdit() @@ -1619,6 +1672,14 @@ class SettingsPage(QWidget): # Soulseek settings soulseek_frame = QFrame() + soulseek_frame.setStyleSheet(""" + QFrame { + background: #333333; + border: 1px solid #444444; + border-radius: 8px; + padding: 8px; + } + """) soulseek_layout = QVBoxLayout(soulseek_frame) soulseek_layout.setSpacing(8) @@ -1629,7 +1690,7 @@ class SettingsPage(QWidget): # slskd URL slskd_url_label = QLabel("slskd URL:") - slskd_url_label.setStyleSheet("color: #ffffff; font-size: 11px;") + slskd_url_label.setStyleSheet(self.get_label_style(11)) soulseek_layout.addWidget(slskd_url_label) url_input_layout = QHBoxLayout() @@ -1649,7 +1710,7 @@ class SettingsPage(QWidget): # API Key api_key_label = QLabel("API Key:") - api_key_label.setStyleSheet("color: #ffffff; font-size: 11px;") + api_key_label.setStyleSheet(self.get_label_style(11)) soulseek_layout.addWidget(api_key_label) self.api_key_input = QLineEdit() @@ -1701,16 +1762,14 @@ class SettingsPage(QWidget): # Log level (read-only) log_level_layout = QHBoxLayout() log_level_label = QLabel("Log Level:") - log_level_label.setStyleSheet("color: #ffffff; font-size: 12px;") + log_level_label.setStyleSheet(self.get_label_style(12)) self.log_level_display = QLabel("DEBUG") self.log_level_display.setStyleSheet(""" - color: #b3b3b3; + color: #ffffff; font-size: 11px; - background-color: #404040; - border: 1px solid #606060; - border-radius: 4px; - padding: 8px; + background: transparent; + border: none; """) self.log_level_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) @@ -1720,18 +1779,18 @@ class SettingsPage(QWidget): # Log file path (read-only) log_path_container = QVBoxLayout() log_path_label = QLabel("Log File Path:") - log_path_label.setStyleSheet("color: #ffffff; font-size: 12px;") + log_path_label.setStyleSheet(self.get_label_style(12)) log_path_container.addWidget(log_path_label) self.log_path_display = QLabel("logs/app.log") self.log_path_display.setStyleSheet(""" - color: #b3b3b3; + color: #1db954; font-size: 11px; - background-color: #404040; - border: 1px solid #606060; - border-radius: 4px; - padding: 8px; font-family: 'Courier New', monospace; + background-color: rgba(29, 185, 84, 0.1); + border: 1px solid rgba(29, 185, 84, 0.3); + border-radius: 4px; + padding: 6px 8px; """) self.log_path_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) log_path_container.addWidget(self.log_path_display) @@ -1759,7 +1818,7 @@ class SettingsPage(QWidget): # Quality preference quality_layout = QHBoxLayout() quality_label = QLabel("Preferred Quality:") - quality_label.setStyleSheet("color: #ffffff; font-size: 12px;") + quality_label.setStyleSheet(self.get_label_style(12)) self.quality_combo = QComboBox() self.quality_combo.addItems(["FLAC", "320 kbps MP3", "256 kbps MP3", "192 kbps MP3", "Any"]) @@ -1774,7 +1833,7 @@ class SettingsPage(QWidget): # Download path path_container = QVBoxLayout() path_label = QLabel("Slskd Download Dir:") - path_label.setStyleSheet("color: #ffffff; font-size: 12px;") + path_label.setStyleSheet(self.get_label_style(12)) path_container.addWidget(path_label) path_input_layout = QHBoxLayout() @@ -1794,7 +1853,7 @@ class SettingsPage(QWidget): # Transfer folder path transfer_path_container = QVBoxLayout() transfer_path_label = QLabel("Matched Transfer Dir (Plex Music Dir?):") - transfer_path_label.setStyleSheet("color: #ffffff; font-size: 12px;") + transfer_path_label.setStyleSheet(self.get_label_style(12)) transfer_path_container.addWidget(transfer_path_label) transfer_input_layout = QHBoxLayout() @@ -1824,7 +1883,7 @@ class SettingsPage(QWidget): # Max Workers workers_layout = QHBoxLayout() workers_label = QLabel("Concurrent Workers:") - workers_label.setStyleSheet("color: #ffffff; font-size: 12px;") + workers_label.setStyleSheet(self.get_label_style(12)) self.max_workers_combo = QComboBox() self.max_workers_combo.addItems(["3", "4", "5", "6", "7", "8", "9", "10"]) @@ -1837,7 +1896,7 @@ class SettingsPage(QWidget): # Help text for workers workers_help = QLabel("Number of parallel threads for database updates. Higher values = faster updates but more server load.") - workers_help.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;") + workers_help.setStyleSheet("color: #ffffff; font-size: 10px; font-style: italic; background: transparent;") workers_help.setWordWrap(True) database_layout.addLayout(workers_layout) @@ -1857,6 +1916,7 @@ class SettingsPage(QWidget): color: #ffffff; font-size: 12px; spacing: 8px; + background: transparent; } QCheckBox::indicator { width: 16px; @@ -1886,16 +1946,14 @@ class SettingsPage(QWidget): # Supported formats display supported_formats_layout = QHBoxLayout() formats_label = QLabel("Supported Formats:") - formats_label.setStyleSheet("color: #ffffff; font-size: 12px;") + formats_label.setStyleSheet(self.get_label_style(12)) formats_display = QLabel("MP3, FLAC, MP4/M4A, OGG") formats_display.setStyleSheet(""" - color: #b3b3b3; + color: #ffffff; font-size: 11px; - background-color: #404040; - border: 1px solid #606060; - border-radius: 4px; - padding: 6px; + background: transparent; + border: none; """) supported_formats_layout.addWidget(formats_label) @@ -1903,7 +1961,7 @@ class SettingsPage(QWidget): # Help text help_text = QLabel("Automatically enhances downloaded tracks with accurate Spotify metadata including artist, album, track numbers, genres, and release dates. Perfect for Plex libraries!") - help_text.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;") + help_text.setStyleSheet("color: #ffffff; font-size: 10px; font-style: italic; background: transparent;") help_text.setWordWrap(True) metadata_layout.addWidget(self.metadata_enabled_checkbox) @@ -1925,6 +1983,7 @@ class SettingsPage(QWidget): color: #ffffff; font-size: 12px; spacing: 8px; + background: transparent; } QCheckBox::indicator { width: 16px; @@ -1945,7 +2004,7 @@ class SettingsPage(QWidget): # Help text for playlist sync playlist_help_text = QLabel("When enabled, existing Plex playlists will be backed up as '[Playlist Name] Backup' before being overwritten during sync. Only one backup per playlist is maintained.") - playlist_help_text.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;") + playlist_help_text.setStyleSheet("color: #ffffff; font-size: 10px; font-style: italic; background: transparent;") playlist_help_text.setWordWrap(True) playlist_sync_layout.addWidget(self.create_backup_checkbox) @@ -2044,4 +2103,15 @@ class SettingsPage(QWidget): QPushButton:hover { background: rgba(29, 185, 84, 0.1); } + """ + + def get_label_style(self, font_size=12): + """Get consistent label style without background""" + return f""" + QLabel {{ + color: #ffffff; + font-size: {font_size}px; + background: transparent; + border: none; + }} """ \ No newline at end of file