wishlist functionality
This commit is contained in:
parent
ed91099a1c
commit
b5e3c47051
10 changed files with 1321 additions and 13 deletions
BIN
core/__pycache__/wishlist_service.cpython-312.pyc
Normal file
BIN
core/__pycache__/wishlist_service.cpython-312.pyc
Normal file
Binary file not shown.
274
core/wishlist_service.py
Normal file
274
core/wishlist_service.py
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Wishlist Service - High-level service for managing failed download track wishlist
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from database.music_database import get_database
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("wishlist_service")
|
||||
|
||||
class WishlistService:
|
||||
"""Service for managing the wishlist of failed download tracks"""
|
||||
|
||||
def __init__(self, database_path: str = "database/music_library.db"):
|
||||
self.database_path = database_path
|
||||
self._database = None
|
||||
|
||||
@property
|
||||
def database(self):
|
||||
"""Get database instance (lazy loading)"""
|
||||
if self._database is None:
|
||||
self._database = get_database(self.database_path)
|
||||
return self._database
|
||||
|
||||
def add_failed_track_from_modal(self, track_info: Dict[str, Any], source_type: str = "unknown",
|
||||
source_context: Dict[str, Any] = None) -> bool:
|
||||
"""
|
||||
Add a failed track from a download modal to the wishlist.
|
||||
|
||||
Args:
|
||||
track_info: Track info dictionary from modal's permanently_failed_tracks
|
||||
source_type: Type of source ('playlist', 'album', 'manual')
|
||||
source_context: Additional context (playlist name, album info, etc.)
|
||||
"""
|
||||
try:
|
||||
# Extract Spotify track data from the track_info structure
|
||||
spotify_track = self._extract_spotify_track_from_modal_info(track_info)
|
||||
if not spotify_track:
|
||||
logger.error(f"Could not extract Spotify track data from modal info")
|
||||
return False
|
||||
|
||||
# Get failure reason from track_info if available
|
||||
failure_reason = track_info.get('failure_reason', 'Download failed')
|
||||
|
||||
# Create source info
|
||||
source_info = source_context or {}
|
||||
source_info['original_modal_data'] = {
|
||||
'download_index': track_info.get('download_index'),
|
||||
'table_index': track_info.get('table_index'),
|
||||
'candidates': track_info.get('candidates', [])
|
||||
}
|
||||
|
||||
# Add to wishlist via database
|
||||
return self.database.add_to_wishlist(
|
||||
spotify_track_data=spotify_track,
|
||||
failure_reason=failure_reason,
|
||||
source_type=source_type,
|
||||
source_info=source_info
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding failed track to wishlist: {e}")
|
||||
return False
|
||||
|
||||
def add_spotify_track_to_wishlist(self, spotify_track_data: Dict[str, Any], failure_reason: str,
|
||||
source_type: str = "manual", source_context: Dict[str, Any] = None) -> bool:
|
||||
"""
|
||||
Directly add a Spotify track to the wishlist.
|
||||
|
||||
Args:
|
||||
spotify_track_data: Full Spotify track data dictionary
|
||||
failure_reason: Reason for the failure
|
||||
source_type: Source type ('playlist', 'album', 'manual')
|
||||
source_context: Additional context information
|
||||
"""
|
||||
return self.database.add_to_wishlist(
|
||||
spotify_track_data=spotify_track_data,
|
||||
failure_reason=failure_reason,
|
||||
source_type=source_type,
|
||||
source_info=source_context or {}
|
||||
)
|
||||
|
||||
def get_wishlist_tracks_for_download(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get wishlist tracks formatted for the download modal.
|
||||
Returns tracks in a format similar to playlist tracks for compatibility.
|
||||
"""
|
||||
try:
|
||||
wishlist_tracks = self.database.get_wishlist_tracks(limit=limit)
|
||||
formatted_tracks = []
|
||||
|
||||
for wishlist_track in wishlist_tracks:
|
||||
spotify_data = wishlist_track['spotify_data']
|
||||
|
||||
# Create a track object similar to what download modals expect
|
||||
formatted_track = {
|
||||
'wishlist_id': wishlist_track['id'],
|
||||
'spotify_track_id': wishlist_track['spotify_track_id'],
|
||||
'spotify_data': spotify_data,
|
||||
'failure_reason': wishlist_track['failure_reason'],
|
||||
'retry_count': wishlist_track['retry_count'],
|
||||
'date_added': wishlist_track['date_added'],
|
||||
'last_attempted': wishlist_track['last_attempted'],
|
||||
'source_type': wishlist_track['source_type'],
|
||||
'source_info': wishlist_track['source_info'],
|
||||
|
||||
# Format for modal compatibility (similar to Spotify Track objects)
|
||||
'id': spotify_data.get('id'),
|
||||
'name': spotify_data.get('name', 'Unknown Track'),
|
||||
'artists': spotify_data.get('artists', []),
|
||||
'album': spotify_data.get('album', {}),
|
||||
'duration_ms': spotify_data.get('duration_ms', 0),
|
||||
'preview_url': spotify_data.get('preview_url'),
|
||||
'external_urls': spotify_data.get('external_urls', {}),
|
||||
'popularity': spotify_data.get('popularity', 0)
|
||||
}
|
||||
|
||||
formatted_tracks.append(formatted_track)
|
||||
|
||||
return formatted_tracks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting wishlist tracks for download: {e}")
|
||||
return []
|
||||
|
||||
def mark_track_download_result(self, spotify_track_id: str, success: bool, error_message: str = None) -> bool:
|
||||
"""
|
||||
Mark the result of a download attempt for a wishlist track.
|
||||
|
||||
Args:
|
||||
spotify_track_id: Spotify track ID
|
||||
success: Whether the download was successful
|
||||
error_message: Error message if failed
|
||||
"""
|
||||
return self.database.update_wishlist_retry(spotify_track_id, success, error_message)
|
||||
|
||||
def remove_track_from_wishlist(self, spotify_track_id: str) -> bool:
|
||||
"""Remove a track from the wishlist (typically after successful download)"""
|
||||
return self.database.remove_from_wishlist(spotify_track_id)
|
||||
|
||||
def get_wishlist_count(self) -> int:
|
||||
"""Get the total number of tracks in the wishlist"""
|
||||
return self.database.get_wishlist_count()
|
||||
|
||||
def clear_wishlist(self) -> bool:
|
||||
"""Clear all tracks from the wishlist"""
|
||||
return self.database.clear_wishlist()
|
||||
|
||||
def get_wishlist_summary(self) -> Dict[str, Any]:
|
||||
"""Get a summary of the wishlist for dashboard display"""
|
||||
try:
|
||||
total_tracks = self.get_wishlist_count()
|
||||
|
||||
if total_tracks == 0:
|
||||
return {
|
||||
'total_tracks': 0,
|
||||
'by_source_type': {},
|
||||
'recent_failures': []
|
||||
}
|
||||
|
||||
# Get detailed breakdown
|
||||
wishlist_tracks = self.database.get_wishlist_tracks()
|
||||
|
||||
# Group by source type
|
||||
by_source_type = {}
|
||||
recent_failures = []
|
||||
|
||||
for track in wishlist_tracks:
|
||||
source_type = track['source_type']
|
||||
by_source_type[source_type] = by_source_type.get(source_type, 0) + 1
|
||||
|
||||
# Keep track of recent failures (last 5)
|
||||
if len(recent_failures) < 5:
|
||||
spotify_data = track['spotify_data']
|
||||
recent_failures.append({
|
||||
'name': spotify_data.get('name', 'Unknown Track'),
|
||||
'artist': spotify_data.get('artists', [{}])[0].get('name', 'Unknown Artist'),
|
||||
'failure_reason': track['failure_reason'],
|
||||
'retry_count': track['retry_count'],
|
||||
'date_added': track['date_added']
|
||||
})
|
||||
|
||||
return {
|
||||
'total_tracks': total_tracks,
|
||||
'by_source_type': by_source_type,
|
||||
'recent_failures': recent_failures
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting wishlist summary: {e}")
|
||||
return {
|
||||
'total_tracks': 0,
|
||||
'by_source_type': {},
|
||||
'recent_failures': []
|
||||
}
|
||||
|
||||
def _extract_spotify_track_from_modal_info(self, track_info: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Extract Spotify track data from modal track_info structure.
|
||||
Handles different formats from sync.py and artists.py modals.
|
||||
"""
|
||||
try:
|
||||
# Try to find Spotify track data in various locations within track_info
|
||||
|
||||
# Check if we have direct Spotify track reference
|
||||
if 'spotify_track' in track_info and track_info['spotify_track']:
|
||||
spotify_track = track_info['spotify_track']
|
||||
|
||||
# Convert to dictionary if it's an object
|
||||
if hasattr(spotify_track, '__dict__'):
|
||||
return self._spotify_track_object_to_dict(spotify_track)
|
||||
elif isinstance(spotify_track, dict):
|
||||
return spotify_track
|
||||
|
||||
# Check if we have slskd_result with embedded metadata
|
||||
if 'slskd_result' in track_info and track_info['slskd_result']:
|
||||
slskd_result = track_info['slskd_result']
|
||||
|
||||
# Look for Spotify metadata in the result
|
||||
if hasattr(slskd_result, 'artist') and hasattr(slskd_result, 'title'):
|
||||
# Reconstruct basic Spotify track structure
|
||||
return {
|
||||
'id': f"reconstructed_{hash(f'{slskd_result.artist}_{slskd_result.title}')}",
|
||||
'name': getattr(slskd_result, 'title', 'Unknown Track'),
|
||||
'artists': [{'name': getattr(slskd_result, 'artist', 'Unknown Artist')}],
|
||||
'album': {'name': getattr(slskd_result, 'album', 'Unknown Album')},
|
||||
'duration_ms': 0, # Unknown
|
||||
'reconstructed': True # Mark as reconstructed data
|
||||
}
|
||||
|
||||
# If no Spotify data found, try to reconstruct from available info
|
||||
logger.warning("Could not find Spotify track data in modal info, attempting reconstruction")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting Spotify track from modal info: {e}")
|
||||
return None
|
||||
|
||||
def _spotify_track_object_to_dict(self, spotify_track) -> Dict[str, Any]:
|
||||
"""Convert a Spotify track object to a dictionary"""
|
||||
try:
|
||||
return {
|
||||
'id': getattr(spotify_track, 'id', None),
|
||||
'name': getattr(spotify_track, 'name', 'Unknown Track'),
|
||||
'artists': [
|
||||
{'name': artist.name if hasattr(artist, 'name') else str(artist)}
|
||||
for artist in getattr(spotify_track, 'artists', [])
|
||||
],
|
||||
'album': {
|
||||
'name': getattr(spotify_track.album, 'name', 'Unknown Album')
|
||||
if hasattr(spotify_track, 'album') and spotify_track.album
|
||||
else 'Unknown Album'
|
||||
},
|
||||
'duration_ms': getattr(spotify_track, 'duration_ms', 0),
|
||||
'preview_url': getattr(spotify_track, 'preview_url', None),
|
||||
'external_urls': getattr(spotify_track, 'external_urls', {}),
|
||||
'popularity': getattr(spotify_track, 'popularity', 0)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting Spotify track object to dict: {e}")
|
||||
return {}
|
||||
|
||||
# Global singleton instance
|
||||
_wishlist_service = None
|
||||
|
||||
def get_wishlist_service() -> WishlistService:
|
||||
"""Get the global wishlist service instance"""
|
||||
global _wishlist_service
|
||||
if _wishlist_service is None:
|
||||
_wishlist_service = WishlistService()
|
||||
return _wishlist_service
|
||||
Binary file not shown.
|
|
@ -135,10 +135,27 @@ class MusicDatabase:
|
|||
)
|
||||
""")
|
||||
|
||||
# Wishlist table for storing failed download tracks for retry
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS wishlist_tracks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
spotify_track_id TEXT UNIQUE NOT NULL,
|
||||
spotify_data TEXT NOT NULL, -- JSON of full Spotify track data
|
||||
failure_reason TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
last_attempted TIMESTAMP,
|
||||
date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
source_type TEXT DEFAULT 'unknown', -- 'playlist', 'album', 'manual'
|
||||
source_info TEXT -- JSON of source context (playlist name, album info, etc.)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for performance
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_artist_id ON albums (artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_artist_id ON tracks (artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_wishlist_spotify_id ON wishlist_tracks (spotify_track_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_wishlist_date_added ON wishlist_tracks (date_added)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_name ON artists (name)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_title ON albums (title)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_title ON tracks (title)")
|
||||
|
|
@ -1372,6 +1389,158 @@ class MusicDatabase:
|
|||
def get_last_full_refresh(self) -> Optional[str]:
|
||||
"""Get the date of the last full refresh"""
|
||||
return self.get_metadata('last_full_refresh')
|
||||
|
||||
# Wishlist management methods
|
||||
|
||||
def add_to_wishlist(self, spotify_track_data: Dict[str, Any], failure_reason: str = "Download failed",
|
||||
source_type: str = "unknown", source_info: Dict[str, Any] = None) -> bool:
|
||||
"""Add a failed track to the wishlist for retry"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Use Spotify track ID as unique identifier
|
||||
track_id = spotify_track_data.get('id')
|
||||
if not track_id:
|
||||
logger.error("Cannot add track to wishlist: missing Spotify track ID")
|
||||
return False
|
||||
|
||||
# Convert data to JSON strings
|
||||
spotify_json = json.dumps(spotify_track_data)
|
||||
source_json = json.dumps(source_info or {})
|
||||
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO wishlist_tracks
|
||||
(spotify_track_id, spotify_data, failure_reason, source_type, source_info, date_added)
|
||||
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (track_id, spotify_json, failure_reason, source_type, source_json))
|
||||
|
||||
conn.commit()
|
||||
|
||||
track_name = spotify_track_data.get('name', 'Unknown Track')
|
||||
artist_name = spotify_track_data.get('artists', [{}])[0].get('name', 'Unknown Artist')
|
||||
logger.info(f"Added track to wishlist: '{track_name}' by {artist_name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding track to wishlist: {e}")
|
||||
return False
|
||||
|
||||
def remove_from_wishlist(self, spotify_track_id: str) -> bool:
|
||||
"""Remove a track from the wishlist (typically after successful download)"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM wishlist_tracks WHERE spotify_track_id = ?", (spotify_track_id,))
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
logger.info(f"Removed track from wishlist: {spotify_track_id}")
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"Track not found in wishlist: {spotify_track_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing track from wishlist: {e}")
|
||||
return False
|
||||
|
||||
def get_wishlist_tracks(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""Get all tracks in the wishlist, ordered by date added (oldest first for retry priority)"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT id, spotify_track_id, spotify_data, failure_reason, retry_count,
|
||||
last_attempted, date_added, source_type, source_info
|
||||
FROM wishlist_tracks
|
||||
ORDER BY date_added ASC
|
||||
"""
|
||||
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
|
||||
cursor.execute(query)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
wishlist_tracks = []
|
||||
for row in rows:
|
||||
try:
|
||||
spotify_data = json.loads(row['spotify_data'])
|
||||
source_info = json.loads(row['source_info']) if row['source_info'] else {}
|
||||
|
||||
wishlist_tracks.append({
|
||||
'id': row['id'],
|
||||
'spotify_track_id': row['spotify_track_id'],
|
||||
'spotify_data': spotify_data,
|
||||
'failure_reason': row['failure_reason'],
|
||||
'retry_count': row['retry_count'],
|
||||
'last_attempted': row['last_attempted'],
|
||||
'date_added': row['date_added'],
|
||||
'source_type': row['source_type'],
|
||||
'source_info': source_info
|
||||
})
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error parsing wishlist track data: {e}")
|
||||
continue
|
||||
|
||||
return wishlist_tracks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting wishlist tracks: {e}")
|
||||
return []
|
||||
|
||||
def update_wishlist_retry(self, spotify_track_id: str, success: bool, error_message: str = None) -> bool:
|
||||
"""Update retry count and status for a wishlist track"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
if success:
|
||||
# Remove from wishlist on success
|
||||
cursor.execute("DELETE FROM wishlist_tracks WHERE spotify_track_id = ?", (spotify_track_id,))
|
||||
else:
|
||||
# Increment retry count and update failure reason
|
||||
cursor.execute("""
|
||||
UPDATE wishlist_tracks
|
||||
SET retry_count = retry_count + 1,
|
||||
last_attempted = CURRENT_TIMESTAMP,
|
||||
failure_reason = COALESCE(?, failure_reason)
|
||||
WHERE spotify_track_id = ?
|
||||
""", (error_message, spotify_track_id))
|
||||
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating wishlist retry status: {e}")
|
||||
return False
|
||||
|
||||
def get_wishlist_count(self) -> int:
|
||||
"""Get the total number of tracks in the wishlist"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM wishlist_tracks")
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting wishlist count: {e}")
|
||||
return 0
|
||||
|
||||
def clear_wishlist(self) -> bool:
|
||||
"""Clear all tracks from the wishlist"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM wishlist_tracks")
|
||||
conn.commit()
|
||||
logger.info(f"Cleared {cursor.rowcount} tracks from wishlist")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing wishlist: {e}")
|
||||
return False
|
||||
|
||||
def get_database_info(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive database information"""
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -18,9 +18,14 @@ from core.spotify_client import SpotifyClient, Artist, Album
|
|||
from core.plex_client import PlexClient
|
||||
from core.soulseek_client import SoulseekClient, AlbumResult
|
||||
from core.matching_engine import MusicMatchingEngine
|
||||
from core.wishlist_service import get_wishlist_service
|
||||
from core.plex_scan_manager import PlexScanManager
|
||||
from database.music_database import get_database
|
||||
from utils.logging_config import get_logger
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
logger = get_logger("artists")
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -1745,6 +1750,7 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
|||
self.downloads_page = downloads_page
|
||||
self.plex_client = plex_client
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
self.wishlist_service = get_wishlist_service()
|
||||
|
||||
# State tracking
|
||||
self.total_tracks = len(album.tracks)
|
||||
|
|
@ -2656,9 +2662,47 @@ class DownloadMissingAlbumTracksModal(QDialog):
|
|||
album_name = getattr(self.album, 'name', 'Unknown Album')
|
||||
self.parent_artists_page.scan_manager.request_scan(f"Album download completed: {album_name} ({self.successful_downloads} tracks)")
|
||||
|
||||
# Add permanently failed tracks to wishlist before showing completion message
|
||||
failed_count = len(self.permanently_failed_tracks)
|
||||
wishlist_added_count = 0
|
||||
|
||||
if self.permanently_failed_tracks:
|
||||
try:
|
||||
# Add failed tracks to wishlist
|
||||
source_context = {
|
||||
'album_name': getattr(self.album, 'name', 'Unknown Album'),
|
||||
'album_id': getattr(self.album, 'id', None),
|
||||
'artist_name': getattr(self.album, 'artists', [{}])[0].get('name', 'Unknown Artist') if hasattr(self.album, 'artists') else 'Unknown Artist',
|
||||
'added_from': 'artists_page_modal',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
for failed_track_info in self.permanently_failed_tracks:
|
||||
try:
|
||||
success = self.wishlist_service.add_failed_track_from_modal(
|
||||
track_info=failed_track_info,
|
||||
source_type='album',
|
||||
source_context=source_context
|
||||
)
|
||||
if success:
|
||||
wishlist_added_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add album track to wishlist: {e}")
|
||||
|
||||
if wishlist_added_count > 0:
|
||||
logger.info(f"Added {wishlist_added_count} failed tracks to wishlist from album '{self.album.name}'")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding failed album tracks to wishlist: {e}")
|
||||
|
||||
# Determine the final message based on success or failure
|
||||
if self.permanently_failed_tracks:
|
||||
final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing album tracks!\n\nYou can now manually correct any failed downloads or close this window."
|
||||
final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing album tracks!\n\n"
|
||||
|
||||
if wishlist_added_count > 0:
|
||||
final_message += f"✨ Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n"
|
||||
|
||||
final_message += "You can also manually correct failed downloads or check the wishlist on the dashboard."
|
||||
|
||||
# If there are failures, ensure the modal is visible and bring it to the front
|
||||
if self.isHidden():
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QFrame, QGridLayout, QScrollArea, QSizePolicy, QPushButton,
|
||||
QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout, QComboBox)
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject
|
||||
QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout, QComboBox,
|
||||
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox)
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject, QRunnable, QThreadPool
|
||||
from PyQt6.QtGui import QFont, QPalette, QColor
|
||||
import time
|
||||
import asyncio
|
||||
|
|
@ -22,10 +23,447 @@ import io
|
|||
from core.matching_engine import MusicMatchingEngine
|
||||
from ui.components.database_updater_widget import DatabaseUpdaterWidget
|
||||
from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker
|
||||
from core.wishlist_service import get_wishlist_service
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("dashboard")
|
||||
|
||||
class DownloadMissingWishlistTracksModal(QDialog):
|
||||
"""Modal for downloading tracks from the wishlist with live progress tracking"""
|
||||
process_finished = pyqtSignal()
|
||||
|
||||
def __init__(self, wishlist_service, parent_dashboard, downloads_page, spotify_client, plex_client, soulseek_client):
|
||||
super().__init__(parent_dashboard)
|
||||
self.wishlist_service = wishlist_service
|
||||
self.parent_dashboard = parent_dashboard
|
||||
self.downloads_page = downloads_page
|
||||
self.spotify_client = spotify_client
|
||||
self.plex_client = plex_client
|
||||
self.soulseek_client = soulseek_client
|
||||
|
||||
# Import matching engine
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
|
||||
# State tracking
|
||||
self.wishlist_tracks = []
|
||||
self.total_tracks = 0
|
||||
self.download_in_progress = False
|
||||
self.cancel_requested = False
|
||||
self.active_parallel_downloads = 0
|
||||
self.download_queue_index = 0
|
||||
self.completed_downloads = 0
|
||||
self.successful_downloads = 0
|
||||
self.failed_downloads = 0
|
||||
|
||||
# Track active downloads and failed tracks
|
||||
self.active_downloads = []
|
||||
self.permanently_failed_tracks = []
|
||||
|
||||
# Parallel search tracking (adapted from sync.py)
|
||||
self.parallel_search_tracking = {}
|
||||
|
||||
self.setup_ui()
|
||||
self.load_wishlist_tracks()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the modal UI (simplified version based on sync.py modal)"""
|
||||
self.setWindowTitle("Download Wishlist Tracks")
|
||||
self.setMinimumSize(800, 600)
|
||||
self.setStyleSheet("""
|
||||
DownloadMissingWishlistTracksModal {
|
||||
background: #191414;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Header
|
||||
header_label = QLabel("Download Missing Wishlist Tracks")
|
||||
header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
header_label.setStyleSheet("color: #ffffff;")
|
||||
|
||||
# Info label
|
||||
self.info_label = QLabel("Loading wishlist tracks...")
|
||||
self.info_label.setFont(QFont("Arial", 11))
|
||||
self.info_label.setStyleSheet("color: #b3b3b3;")
|
||||
|
||||
# Track table (simplified)
|
||||
self.track_table = QTableWidget()
|
||||
self.track_table.setColumnCount(4)
|
||||
self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Retry Count", "Status"])
|
||||
self.track_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.track_table.setStyleSheet("""
|
||||
QTableWidget {
|
||||
background: #282828;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 8px;
|
||||
gridline-color: #404040;
|
||||
}
|
||||
QTableWidget::item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background: #404040;
|
||||
color: #ffffff;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
|
||||
# Progress bar
|
||||
self.download_progress = QProgressBar()
|
||||
self.download_progress.setVisible(False)
|
||||
self.download_progress.setStyleSheet("""
|
||||
QProgressBar {
|
||||
border: 1px solid #404040;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
background: #282828;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background: #1db954;
|
||||
border-radius: 7px;
|
||||
}
|
||||
""")
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.begin_download_btn = QPushButton("🚀 Begin Downloads")
|
||||
self.begin_download_btn.setFixedHeight(40)
|
||||
self.begin_download_btn.clicked.connect(self.start_downloads)
|
||||
self.begin_download_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #1db954;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #1ed760;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background: #404040;
|
||||
color: #888888;
|
||||
}
|
||||
""")
|
||||
|
||||
self.cancel_btn = QPushButton("Cancel")
|
||||
self.cancel_btn.setFixedHeight(40)
|
||||
self.cancel_btn.clicked.connect(self.on_cancel_clicked)
|
||||
self.cancel_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #404040;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #505050;
|
||||
}
|
||||
""")
|
||||
|
||||
self.clear_wishlist_btn = QPushButton("🗑️ Clear Wishlist")
|
||||
self.clear_wishlist_btn.setFixedHeight(40)
|
||||
self.clear_wishlist_btn.clicked.connect(self.clear_wishlist)
|
||||
self.clear_wishlist_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #e22134;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #ff4757;
|
||||
}
|
||||
""")
|
||||
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.clear_wishlist_btn)
|
||||
button_layout.addWidget(self.cancel_btn)
|
||||
button_layout.addWidget(self.begin_download_btn)
|
||||
|
||||
# Add to layout
|
||||
layout.addWidget(header_label)
|
||||
layout.addWidget(self.info_label)
|
||||
layout.addWidget(self.track_table)
|
||||
layout.addWidget(self.download_progress)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def load_wishlist_tracks(self):
|
||||
"""Load tracks from wishlist"""
|
||||
try:
|
||||
self.wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download()
|
||||
self.total_tracks = len(self.wishlist_tracks)
|
||||
|
||||
if self.total_tracks == 0:
|
||||
self.info_label.setText("No tracks in wishlist")
|
||||
self.begin_download_btn.setEnabled(False)
|
||||
return
|
||||
|
||||
self.info_label.setText(f"Found {self.total_tracks} tracks in wishlist ready for retry")
|
||||
|
||||
# Populate table
|
||||
self.track_table.setRowCount(self.total_tracks)
|
||||
|
||||
for i, track_data in enumerate(self.wishlist_tracks):
|
||||
# Track name
|
||||
self.track_table.setItem(i, 0, QTableWidgetItem(track_data.get('name', 'Unknown Track')))
|
||||
|
||||
# Artist
|
||||
artists = track_data.get('artists', [])
|
||||
artist_name = artists[0].get('name', 'Unknown Artist') if artists else 'Unknown Artist'
|
||||
self.track_table.setItem(i, 1, QTableWidgetItem(artist_name))
|
||||
|
||||
# Retry count
|
||||
retry_count = track_data.get('retry_count', 0)
|
||||
self.track_table.setItem(i, 2, QTableWidgetItem(str(retry_count)))
|
||||
|
||||
# Status
|
||||
self.track_table.setItem(i, 3, QTableWidgetItem("Pending"))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading wishlist tracks: {e}")
|
||||
self.info_label.setText(f"Error loading tracks: {str(e)}")
|
||||
|
||||
def start_downloads(self):
|
||||
"""Start downloading all wishlist tracks"""
|
||||
try:
|
||||
if self.total_tracks == 0:
|
||||
return
|
||||
|
||||
self.download_in_progress = True
|
||||
self.cancel_requested = False
|
||||
self.begin_download_btn.setEnabled(False)
|
||||
self.download_progress.setVisible(True)
|
||||
self.download_progress.setMaximum(self.total_tracks)
|
||||
self.download_progress.setValue(0)
|
||||
|
||||
# Start parallel downloads (simplified approach)
|
||||
self.active_parallel_downloads = 0
|
||||
self.download_queue_index = 0
|
||||
self.completed_downloads = 0
|
||||
self.successful_downloads = 0
|
||||
self.failed_downloads = 0
|
||||
|
||||
# Initialize tracking
|
||||
self.active_downloads = []
|
||||
self.permanently_failed_tracks = []
|
||||
self.parallel_search_tracking = {}
|
||||
|
||||
self.start_next_batch_of_downloads()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting downloads: {e}")
|
||||
QMessageBox.critical(self, "Error", f"Failed to start downloads: {str(e)}")
|
||||
|
||||
def start_next_batch_of_downloads(self, max_concurrent=3):
|
||||
"""Start the next batch of downloads up to the concurrent limit"""
|
||||
while (self.active_parallel_downloads < max_concurrent and
|
||||
self.download_queue_index < len(self.wishlist_tracks)):
|
||||
track_data = self.wishlist_tracks[self.download_queue_index]
|
||||
track_index = self.download_queue_index
|
||||
|
||||
# Update UI
|
||||
self.track_table.setItem(track_index, 3, QTableWidgetItem("🔍 Searching..."))
|
||||
|
||||
# Start search and download for this track (simplified)
|
||||
self.search_and_download_track_simple(track_data, self.download_queue_index)
|
||||
|
||||
self.active_parallel_downloads += 1
|
||||
self.download_queue_index += 1
|
||||
|
||||
if (self.download_queue_index >= len(self.wishlist_tracks) and self.active_parallel_downloads == 0):
|
||||
self.on_all_downloads_complete()
|
||||
|
||||
def search_and_download_track_simple(self, track_data, download_index):
|
||||
"""Simplified search and download for wishlist tracks"""
|
||||
try:
|
||||
# Create a simple search worker
|
||||
artist_name = track_data.get('artists', [{}])[0].get('name', '') if track_data.get('artists') else ''
|
||||
track_name = track_data.get('name', '')
|
||||
|
||||
if not track_name:
|
||||
self.on_track_download_failed(download_index, "Missing track name")
|
||||
return
|
||||
|
||||
# Create search query
|
||||
query = f"{artist_name} {track_name}".strip()
|
||||
if not query:
|
||||
self.on_track_download_failed(download_index, "Cannot create search query")
|
||||
return
|
||||
|
||||
# Use a simple approach - directly call soulseek search
|
||||
worker = SimpleWishlistDownloadWorker(self.soulseek_client, query, track_data, download_index)
|
||||
worker.signals.download_completed.connect(self.on_track_download_completed)
|
||||
worker.signals.download_failed.connect(self.on_track_download_failed)
|
||||
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting track download: {e}")
|
||||
self.on_track_download_failed(download_index, str(e))
|
||||
|
||||
def on_track_download_completed(self, download_index, download_id):
|
||||
"""Handle successful track download"""
|
||||
try:
|
||||
track_data = self.wishlist_tracks[download_index]
|
||||
track_id = track_data.get('spotify_track_id')
|
||||
|
||||
# Update UI
|
||||
self.track_table.setItem(download_index, 3, QTableWidgetItem("✅ Downloaded"))
|
||||
|
||||
# Mark as successful in wishlist service
|
||||
if track_id:
|
||||
self.wishlist_service.mark_track_download_result(track_id, success=True)
|
||||
|
||||
self.successful_downloads += 1
|
||||
self.completed_downloads += 1
|
||||
self.active_parallel_downloads -= 1
|
||||
|
||||
# Update progress
|
||||
self.download_progress.setValue(self.completed_downloads)
|
||||
|
||||
# Continue with next downloads
|
||||
self.start_next_batch_of_downloads()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling download completion: {e}")
|
||||
|
||||
def on_track_download_failed(self, download_index, error_message):
|
||||
"""Handle failed track download"""
|
||||
try:
|
||||
track_data = self.wishlist_tracks[download_index]
|
||||
track_id = track_data.get('spotify_track_id')
|
||||
|
||||
# Update UI
|
||||
self.track_table.setItem(download_index, 3, QTableWidgetItem("❌ Failed"))
|
||||
|
||||
# Mark as failed in wishlist service (increment retry count)
|
||||
if track_id:
|
||||
self.wishlist_service.mark_track_download_result(track_id, success=False, error_message=error_message)
|
||||
|
||||
self.failed_downloads += 1
|
||||
self.completed_downloads += 1
|
||||
self.active_parallel_downloads -= 1
|
||||
|
||||
# Update progress
|
||||
self.download_progress.setValue(self.completed_downloads)
|
||||
|
||||
# Continue with next downloads
|
||||
self.start_next_batch_of_downloads()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling download failure: {e}")
|
||||
|
||||
def on_all_downloads_complete(self):
|
||||
"""Handle completion of all downloads"""
|
||||
try:
|
||||
self.download_in_progress = False
|
||||
|
||||
# Show completion message
|
||||
message = f"Wishlist processing complete!\n\n"
|
||||
message += f"Successfully downloaded: {self.successful_downloads}\n"
|
||||
message += f"Failed: {self.failed_downloads}\n"
|
||||
message += f"Total processed: {self.completed_downloads}\n\n"
|
||||
|
||||
if self.failed_downloads > 0:
|
||||
message += "Failed tracks remain in wishlist for future retry."
|
||||
else:
|
||||
message += "All tracks downloaded successfully!"
|
||||
|
||||
QMessageBox.information(self, "Downloads Complete", message)
|
||||
|
||||
# Emit signal to update parent
|
||||
self.process_finished.emit()
|
||||
|
||||
# Close modal
|
||||
self.accept()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling downloads completion: {e}")
|
||||
|
||||
def clear_wishlist(self):
|
||||
"""Clear all tracks from wishlist"""
|
||||
try:
|
||||
reply = QMessageBox.question(
|
||||
self, "Clear Wishlist",
|
||||
"Are you sure you want to clear all tracks from the wishlist?\n\nThis action cannot be undone.",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
if self.wishlist_service.clear_wishlist():
|
||||
QMessageBox.information(self, "Wishlist Cleared", "All tracks have been removed from the wishlist.")
|
||||
self.process_finished.emit() # Update parent count
|
||||
self.accept() # Close modal
|
||||
else:
|
||||
QMessageBox.warning(self, "Error", "Failed to clear wishlist.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing wishlist: {e}")
|
||||
QMessageBox.critical(self, "Error", f"Failed to clear wishlist: {str(e)}")
|
||||
|
||||
def on_cancel_clicked(self):
|
||||
"""Handle cancel button"""
|
||||
self.cancel_requested = True
|
||||
self.process_finished.emit()
|
||||
self.reject()
|
||||
|
||||
|
||||
class SimpleWishlistDownloadWorker(QRunnable):
|
||||
"""Simple worker to download a single wishlist track"""
|
||||
|
||||
class Signals(QObject):
|
||||
download_completed = pyqtSignal(int, str) # download_index, download_id
|
||||
download_failed = pyqtSignal(int, str) # download_index, error_message
|
||||
|
||||
def __init__(self, soulseek_client, query, track_data, download_index):
|
||||
super().__init__()
|
||||
self.soulseek_client = soulseek_client
|
||||
self.query = query
|
||||
self.track_data = track_data
|
||||
self.download_index = download_index
|
||||
self.signals = self.Signals()
|
||||
|
||||
def run(self):
|
||||
"""Run the download"""
|
||||
try:
|
||||
# Get quality preference
|
||||
from config.settings import config_manager
|
||||
quality_preference = config_manager.get_quality_preference()
|
||||
|
||||
# Use async method in sync context
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
download_id = loop.run_until_complete(
|
||||
self.soulseek_client.search_and_download_best(self.query, quality_preference)
|
||||
)
|
||||
|
||||
if download_id:
|
||||
self.signals.download_completed.emit(self.download_index, download_id)
|
||||
else:
|
||||
self.signals.download_failed.emit(self.download_index, "No search results found")
|
||||
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
except Exception as e:
|
||||
self.signals.download_failed.emit(self.download_index, str(e))
|
||||
|
||||
|
||||
class MetadataUpdateWorker(QThread):
|
||||
"""Worker thread for updating Plex artist metadata using Spotify data"""
|
||||
progress_updated = pyqtSignal(str, int, int, float) # current_artist, processed, total, percentage
|
||||
|
|
@ -562,9 +1000,9 @@ class DashboardDataProvider(QObject):
|
|||
|
||||
def set_service_clients(self, spotify_client, plex_client, soulseek_client):
|
||||
self.service_clients = {
|
||||
'spotify': spotify_client,
|
||||
'plex': plex_client,
|
||||
'soulseek': soulseek_client
|
||||
'spotify_client': spotify_client,
|
||||
'plex_client': plex_client,
|
||||
'soulseek_client': soulseek_client
|
||||
}
|
||||
|
||||
def set_page_references(self, downloads_page, sync_page):
|
||||
|
|
@ -1214,15 +1652,43 @@ class DashboardPage(QWidget):
|
|||
# Initialize list to track active stats workers
|
||||
self._active_stats_workers = []
|
||||
|
||||
# Initialize wishlist service and timers
|
||||
self.wishlist_service = get_wishlist_service()
|
||||
|
||||
# Timer for updating wishlist button count
|
||||
self.wishlist_update_timer = QTimer()
|
||||
self.wishlist_update_timer.timeout.connect(self.update_wishlist_button_count)
|
||||
self.wishlist_update_timer.start(30000) # Update every 30 seconds
|
||||
|
||||
# Timer for automatic wishlist retry processing
|
||||
self.wishlist_retry_timer = QTimer()
|
||||
self.wishlist_retry_timer.timeout.connect(self.process_wishlist_automatically)
|
||||
self.wishlist_retry_timer.start(3600000) # Process every hour (3600000 ms)
|
||||
|
||||
# Track if automatic processing is currently running
|
||||
self.auto_processing_wishlist = False
|
||||
|
||||
# Load initial database statistics (with delay to avoid startup issues)
|
||||
QTimer.singleShot(1000, self.refresh_database_statistics)
|
||||
# Load initial wishlist count (with slight delay)
|
||||
QTimer.singleShot(1500, self.update_wishlist_button_count)
|
||||
|
||||
def set_service_clients(self, spotify_client, plex_client, soulseek_client):
|
||||
def set_service_clients(self, spotify_client, plex_client, soulseek_client, downloads_page=None):
|
||||
"""Called from main window to provide service client references"""
|
||||
self.data_provider.set_service_clients(spotify_client, plex_client, soulseek_client)
|
||||
|
||||
# Store service clients for wishlist modal
|
||||
self.service_clients = {
|
||||
'spotify_client': spotify_client,
|
||||
'plex_client': plex_client,
|
||||
'soulseek_client': soulseek_client,
|
||||
'downloads_page': downloads_page
|
||||
}
|
||||
|
||||
def set_page_references(self, downloads_page, sync_page):
|
||||
"""Called from main window to provide page references for live data"""
|
||||
self.downloads_page = downloads_page
|
||||
self.sync_page = sync_page
|
||||
self.data_provider.set_page_references(downloads_page, sync_page)
|
||||
|
||||
def set_app_start_time(self, start_time):
|
||||
|
|
@ -1302,9 +1768,15 @@ class DashboardPage(QWidget):
|
|||
|
||||
def create_header(self):
|
||||
header = QWidget()
|
||||
layout = QVBoxLayout(header)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
main_layout = QHBoxLayout(header)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(20)
|
||||
|
||||
# Left side - Title and subtitle
|
||||
left_widget = QWidget()
|
||||
left_layout = QVBoxLayout(left_widget)
|
||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||
left_layout.setSpacing(5)
|
||||
|
||||
# Welcome message
|
||||
welcome_label = QLabel("System Dashboard")
|
||||
|
|
@ -1316,8 +1788,52 @@ class DashboardPage(QWidget):
|
|||
subtitle_label.setFont(QFont("Arial", 14))
|
||||
subtitle_label.setStyleSheet("color: #b3b3b3;")
|
||||
|
||||
layout.addWidget(welcome_label)
|
||||
layout.addWidget(subtitle_label)
|
||||
left_layout.addWidget(welcome_label)
|
||||
left_layout.addWidget(subtitle_label)
|
||||
|
||||
# Right side - Wishlist button
|
||||
right_widget = QWidget()
|
||||
right_layout = QVBoxLayout(right_widget)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(0)
|
||||
|
||||
# Spacer to align button with title
|
||||
right_layout.addStretch()
|
||||
|
||||
# Wishlist button
|
||||
self.wishlist_button = QPushButton("🎵 Wishlist (0)")
|
||||
self.wishlist_button.setFixedHeight(45)
|
||||
self.wishlist_button.setFixedWidth(150)
|
||||
self.wishlist_button.clicked.connect(self.on_wishlist_button_clicked)
|
||||
self.wishlist_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #1db954;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #1ed760;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: #169c46;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background: #404040;
|
||||
color: #666666;
|
||||
}
|
||||
""")
|
||||
|
||||
right_layout.addWidget(self.wishlist_button)
|
||||
right_layout.addStretch()
|
||||
|
||||
# Add to main layout
|
||||
main_layout.addWidget(left_widget)
|
||||
main_layout.addStretch() # Push button to the right
|
||||
main_layout.addWidget(right_widget)
|
||||
|
||||
return header
|
||||
|
||||
|
|
@ -1881,6 +2397,13 @@ class DashboardPage(QWidget):
|
|||
def closeEvent(self, event):
|
||||
"""Clean up threads when dashboard is closed"""
|
||||
self.cleanup_threads()
|
||||
|
||||
# Stop wishlist timers
|
||||
if hasattr(self, 'wishlist_update_timer'):
|
||||
self.wishlist_update_timer.stop()
|
||||
if hasattr(self, 'wishlist_retry_timer'):
|
||||
self.wishlist_retry_timer.stop()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
def cleanup_threads(self):
|
||||
|
|
@ -1892,6 +2415,261 @@ class DashboardPage(QWidget):
|
|||
thread.wait(1000) # Wait up to 1 second
|
||||
thread.deleteLater()
|
||||
self.data_provider._test_threads.clear()
|
||||
|
||||
def on_wishlist_button_clicked(self):
|
||||
"""Handle wishlist button click - open wishlist modal"""
|
||||
try:
|
||||
summary = self.wishlist_service.get_wishlist_summary()
|
||||
total_tracks = summary['total_tracks']
|
||||
|
||||
if total_tracks == 0:
|
||||
QMessageBox.information(self, "Wishlist", "Your wishlist is empty!\n\nFailed download tracks will be automatically added here for retry.")
|
||||
return
|
||||
|
||||
# Need to get service clients to pass to modal
|
||||
if not hasattr(self, 'service_clients'):
|
||||
QMessageBox.warning(self, "Wishlist", "Service clients not initialized. Please restart the application.")
|
||||
return
|
||||
|
||||
spotify_client = self.service_clients.get('spotify_client')
|
||||
plex_client = self.service_clients.get('plex_client')
|
||||
soulseek_client = self.service_clients.get('soulseek_client')
|
||||
downloads_page = self.downloads_page
|
||||
|
||||
if not all([spotify_client, plex_client, soulseek_client, downloads_page]):
|
||||
QMessageBox.warning(self, "Wishlist", "Required services not available. Please check your service connections.")
|
||||
return
|
||||
|
||||
# Create and show the wishlist download modal
|
||||
modal = DownloadMissingWishlistTracksModal(
|
||||
self.wishlist_service,
|
||||
self,
|
||||
downloads_page,
|
||||
spotify_client,
|
||||
plex_client,
|
||||
soulseek_client
|
||||
)
|
||||
modal.process_finished.connect(self.update_wishlist_button_count) # Update count when done
|
||||
modal.exec()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening wishlist: {e}")
|
||||
QMessageBox.critical(self, "Error", f"Failed to open wishlist: {str(e)}")
|
||||
|
||||
def update_wishlist_button_count(self):
|
||||
"""Update the wishlist button with current count"""
|
||||
try:
|
||||
count = self.wishlist_service.get_wishlist_count()
|
||||
|
||||
if hasattr(self, 'wishlist_button'):
|
||||
self.wishlist_button.setText(f"🎵 Wishlist ({count})")
|
||||
|
||||
# Enable/disable button based on count
|
||||
if count == 0:
|
||||
self.wishlist_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #404040;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
color: #888888;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #505050;
|
||||
color: #999999;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.wishlist_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #1db954;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #1ed760;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: #169c46;
|
||||
}
|
||||
""")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating wishlist button count: {e}")
|
||||
|
||||
def process_wishlist_automatically(self):
|
||||
"""Automatically process wishlist tracks in the background"""
|
||||
try:
|
||||
# Skip if already processing or no service clients available
|
||||
if self.auto_processing_wishlist:
|
||||
logger.debug("Wishlist auto-processing already running, skipping")
|
||||
return
|
||||
|
||||
if not hasattr(self, 'service_clients') or not self.service_clients:
|
||||
logger.debug("Service clients not available for wishlist auto-processing")
|
||||
return
|
||||
|
||||
# Check if we have tracks to process
|
||||
wishlist_count = self.wishlist_service.get_wishlist_count()
|
||||
if wishlist_count == 0:
|
||||
logger.debug("No tracks in wishlist for auto-processing")
|
||||
return
|
||||
|
||||
# Get service clients
|
||||
spotify_client = self.service_clients.get('spotify_client')
|
||||
plex_client = self.service_clients.get('plex_client')
|
||||
soulseek_client = self.service_clients.get('soulseek_client')
|
||||
downloads_page = self.downloads_page
|
||||
|
||||
if not all([spotify_client, plex_client, soulseek_client, downloads_page]):
|
||||
logger.warning("Required services not available for wishlist auto-processing")
|
||||
return
|
||||
|
||||
logger.info(f"Starting automatic wishlist processing for {wishlist_count} tracks")
|
||||
self.auto_processing_wishlist = True
|
||||
|
||||
# Create and run the background processing worker
|
||||
worker = AutoWishlistProcessorWorker(
|
||||
self.wishlist_service,
|
||||
spotify_client,
|
||||
plex_client,
|
||||
soulseek_client,
|
||||
downloads_page
|
||||
)
|
||||
worker.signals.processing_complete.connect(self.on_auto_wishlist_processing_complete)
|
||||
worker.signals.processing_error.connect(self.on_auto_wishlist_processing_error)
|
||||
|
||||
# Run in thread pool
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting automatic wishlist processing: {e}")
|
||||
self.auto_processing_wishlist = False
|
||||
|
||||
def on_auto_wishlist_processing_complete(self, successful, failed, total):
|
||||
"""Handle completion of automatic wishlist processing"""
|
||||
try:
|
||||
self.auto_processing_wishlist = False
|
||||
|
||||
logger.info(f"Automatic wishlist processing complete: {successful} successful, {failed} failed, {total} total")
|
||||
|
||||
# Update button count since tracks may have been removed
|
||||
self.update_wishlist_button_count()
|
||||
|
||||
# Show toast notification if there were successful downloads
|
||||
if successful > 0 and hasattr(self, 'toast_manager') and self.toast_manager:
|
||||
message = f"Found {successful} wishlist track{'s' if successful != 1 else ''} automatically!"
|
||||
self.toast_manager.success(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling automatic wishlist processing completion: {e}")
|
||||
|
||||
def on_auto_wishlist_processing_error(self, error_message):
|
||||
"""Handle error in automatic wishlist processing"""
|
||||
try:
|
||||
self.auto_processing_wishlist = False
|
||||
logger.error(f"Automatic wishlist processing failed: {error_message}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling automatic wishlist processing error: {e}")
|
||||
|
||||
|
||||
class AutoWishlistProcessorWorker(QRunnable):
|
||||
"""Background worker for automatic wishlist processing"""
|
||||
|
||||
class Signals(QObject):
|
||||
processing_complete = pyqtSignal(int, int, int) # successful, failed, total
|
||||
processing_error = pyqtSignal(str) # error_message
|
||||
|
||||
def __init__(self, wishlist_service, spotify_client, plex_client, soulseek_client, downloads_page):
|
||||
super().__init__()
|
||||
self.wishlist_service = wishlist_service
|
||||
self.spotify_client = spotify_client
|
||||
self.plex_client = plex_client
|
||||
self.soulseek_client = soulseek_client
|
||||
self.downloads_page = downloads_page
|
||||
self.signals = self.Signals()
|
||||
|
||||
def run(self):
|
||||
"""Run automatic wishlist processing"""
|
||||
try:
|
||||
# Get quality preference
|
||||
from config.settings import config_manager
|
||||
quality_preference = config_manager.get_quality_preference()
|
||||
|
||||
# Get wishlist tracks (limit to prevent overwhelming the system)
|
||||
wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download(limit=10)
|
||||
|
||||
if not wishlist_tracks:
|
||||
self.signals.processing_complete.emit(0, 0, 0)
|
||||
return
|
||||
|
||||
total_tracks = len(wishlist_tracks)
|
||||
successful_downloads = 0
|
||||
failed_downloads = 0
|
||||
|
||||
logger.info(f"Processing {total_tracks} wishlist tracks automatically")
|
||||
|
||||
# Process each track
|
||||
for track_data in wishlist_tracks:
|
||||
try:
|
||||
# Create search query
|
||||
artist_name = track_data.get('artists', [{}])[0].get('name', '') if track_data.get('artists') else ''
|
||||
track_name = track_data.get('name', '')
|
||||
|
||||
if not track_name:
|
||||
failed_downloads += 1
|
||||
continue
|
||||
|
||||
query = f"{artist_name} {track_name}".strip()
|
||||
if not query:
|
||||
failed_downloads += 1
|
||||
continue
|
||||
|
||||
# Attempt download
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
download_id = loop.run_until_complete(
|
||||
self.soulseek_client.search_and_download_best(query, quality_preference)
|
||||
)
|
||||
|
||||
track_id = track_data.get('spotify_track_id')
|
||||
|
||||
if download_id and track_id:
|
||||
# Mark as successful (removes from wishlist)
|
||||
self.wishlist_service.mark_track_download_result(track_id, success=True)
|
||||
successful_downloads += 1
|
||||
logger.info(f"Auto-downloaded wishlist track: '{track_name}' by {artist_name}")
|
||||
else:
|
||||
# Mark as failed (increment retry count)
|
||||
if track_id:
|
||||
self.wishlist_service.mark_track_download_result(track_id, success=False, error_message="No search results found")
|
||||
failed_downloads += 1
|
||||
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing wishlist track '{track_name}': {e}")
|
||||
|
||||
# Mark as failed
|
||||
track_id = track_data.get('spotify_track_id')
|
||||
if track_id:
|
||||
self.wishlist_service.mark_track_download_result(track_id, success=False, error_message=str(e))
|
||||
failed_downloads += 1
|
||||
|
||||
# Emit completion
|
||||
self.signals.processing_complete.emit(successful_downloads, failed_downloads, total_tracks)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in automatic wishlist processing: {e}")
|
||||
self.signals.processing_error.emit(str(e))
|
||||
|
||||
# Stop the data provider timers
|
||||
if hasattr(self.data_provider, 'download_stats_timer'):
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ from core.soulseek_client import TrackResult
|
|||
import re
|
||||
import asyncio
|
||||
from core.matching_engine import MusicMatchingEngine
|
||||
from core.wishlist_service import get_wishlist_service
|
||||
from ui.components.toast_manager import ToastType
|
||||
from database.music_database import get_database
|
||||
from core.plex_scan_manager import PlexScanManager
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("sync")
|
||||
|
||||
# Define constants for storage
|
||||
STORAGE_DIR = "storage"
|
||||
|
|
@ -3574,6 +3578,8 @@ class DownloadMissingTracksModal(QDialog):
|
|||
self.parent_sync_page = parent_page # Reference to sync page for scan manager
|
||||
self.downloads_page = downloads_page
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
self.wishlist_service = get_wishlist_service()
|
||||
|
||||
# State tracking
|
||||
self.total_tracks = len(playlist.tracks)
|
||||
self.matched_tracks_count = 0
|
||||
|
|
@ -4461,9 +4467,46 @@ class DownloadMissingTracksModal(QDialog):
|
|||
if self.successful_downloads > 0 and hasattr(self, 'parent_sync_page') and self.parent_sync_page.scan_manager:
|
||||
self.parent_sync_page.scan_manager.request_scan(f"Playlist download completed ({self.successful_downloads} tracks)")
|
||||
|
||||
# Add permanently failed tracks to wishlist before showing completion message
|
||||
failed_count = len(self.permanently_failed_tracks)
|
||||
wishlist_added_count = 0
|
||||
|
||||
if self.permanently_failed_tracks:
|
||||
try:
|
||||
# Add failed tracks to wishlist
|
||||
source_context = {
|
||||
'playlist_name': getattr(self.playlist, 'name', 'Unknown Playlist'),
|
||||
'playlist_id': getattr(self.playlist, 'id', None),
|
||||
'added_from': 'sync_page_modal',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
for failed_track_info in self.permanently_failed_tracks:
|
||||
try:
|
||||
success = self.wishlist_service.add_failed_track_from_modal(
|
||||
track_info=failed_track_info,
|
||||
source_type='playlist',
|
||||
source_context=source_context
|
||||
)
|
||||
if success:
|
||||
wishlist_added_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add track to wishlist: {e}")
|
||||
|
||||
if wishlist_added_count > 0:
|
||||
logger.info(f"Added {wishlist_added_count} failed tracks to wishlist from playlist '{self.playlist.name}'")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding failed tracks to wishlist: {e}")
|
||||
|
||||
# Determine the final message based on success or failure.
|
||||
if self.permanently_failed_tracks:
|
||||
final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing tracks!\n\nYou can now manually correct any failed downloads or close this window."
|
||||
final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing tracks!\n\n"
|
||||
|
||||
if wishlist_added_count > 0:
|
||||
final_message += f"✨ Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n"
|
||||
|
||||
final_message += "You can also manually correct failed downloads or check the wishlist on the dashboard."
|
||||
|
||||
# If there are failures, ensure the modal is visible and bring it to the front.
|
||||
if self.isHidden():
|
||||
|
|
|
|||
Loading…
Reference in a new issue