fix settings page, add rate limiting, live tracking to watchlist modal

This commit is contained in:
Broque Thomas 2025-08-16 23:28:29 -07:00
parent 1bf7995864
commit 8063197a54
5 changed files with 443 additions and 364 deletions

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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