|
|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
|
|
|
QFrame, QGridLayout, QScrollArea, QSizePolicy, QPushButton,
|
|
|
|
|
QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout, QComboBox,
|
|
|
|
|
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox)
|
|
|
|
|
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QMessageBox, QApplication)
|
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject, QRunnable, QThreadPool
|
|
|
|
|
from PyQt6.QtGui import QFont, QPalette, QColor
|
|
|
|
|
import time
|
|
|
|
|
@ -64,138 +64,270 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
|
|
|
|
|
self.setup_ui()
|
|
|
|
|
self.load_wishlist_tracks()
|
|
|
|
|
|
|
|
|
|
# Timer to periodically check for automatic processing status changes
|
|
|
|
|
self.status_check_timer = QTimer()
|
|
|
|
|
self.status_check_timer.timeout.connect(self.check_auto_processing_status)
|
|
|
|
|
self.status_check_timer.start(2000) # Check every 2 seconds
|
|
|
|
|
|
|
|
|
|
def setup_ui(self):
|
|
|
|
|
"""Setup the modal UI (simplified version based on sync.py modal)"""
|
|
|
|
|
self.setWindowTitle("Download Wishlist Tracks")
|
|
|
|
|
self.setMinimumSize(800, 600)
|
|
|
|
|
"""Setup the modal UI with enhanced styling"""
|
|
|
|
|
self.setWindowTitle("Wishlist Downloads")
|
|
|
|
|
self.setMinimumSize(900, 650)
|
|
|
|
|
self.setStyleSheet("""
|
|
|
|
|
DownloadMissingWishlistTracksModal {
|
|
|
|
|
background: #191414;
|
|
|
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
|
|
|
stop: 0 #1a1a1a,
|
|
|
|
|
stop: 1 #0f0f0f);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
|
layout.setSpacing(15)
|
|
|
|
|
layout.setContentsMargins(24, 24, 24, 24)
|
|
|
|
|
layout.setSpacing(20)
|
|
|
|
|
|
|
|
|
|
# Header
|
|
|
|
|
header_label = QLabel("Download Missing Wishlist Tracks")
|
|
|
|
|
header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
|
|
|
|
header_label.setStyleSheet("color: #ffffff;")
|
|
|
|
|
# Header section with icon
|
|
|
|
|
header_layout = QHBoxLayout()
|
|
|
|
|
header_layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
# Info label
|
|
|
|
|
# Wishlist icon
|
|
|
|
|
icon_label = QLabel("🎵")
|
|
|
|
|
icon_label.setFont(QFont("Arial", 20))
|
|
|
|
|
icon_label.setFixedSize(32, 32)
|
|
|
|
|
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
|
icon_label.setStyleSheet("""
|
|
|
|
|
QLabel {
|
|
|
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
|
|
|
stop: 0 rgba(29, 185, 84, 0.15),
|
|
|
|
|
stop: 1 rgba(30, 215, 96, 0.1));
|
|
|
|
|
border: 1px solid rgba(29, 185, 84, 0.3);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Header text
|
|
|
|
|
header_label = QLabel("Wishlist Downloads")
|
|
|
|
|
header_label.setFont(QFont("SF Pro Display", 18, QFont.Weight.Bold))
|
|
|
|
|
header_label.setStyleSheet("""
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: 0.3px;
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
header_layout.addWidget(icon_label)
|
|
|
|
|
header_layout.addWidget(header_label)
|
|
|
|
|
header_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
# Info label with better styling
|
|
|
|
|
self.info_label = QLabel("Loading wishlist tracks...")
|
|
|
|
|
self.info_label.setFont(QFont("Arial", 11))
|
|
|
|
|
self.info_label.setStyleSheet("color: #b3b3b3;")
|
|
|
|
|
self.info_label.setFont(QFont("SF Pro Text", 12))
|
|
|
|
|
self.info_label.setStyleSheet("""
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
padding: 8px 0px;
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Track table (simplified)
|
|
|
|
|
# Enhanced track table
|
|
|
|
|
self.track_table = QTableWidget()
|
|
|
|
|
self.track_table.setColumnCount(4)
|
|
|
|
|
self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Retry Count", "Status"])
|
|
|
|
|
self.track_table.horizontalHeader().setStretchLastSection(True)
|
|
|
|
|
|
|
|
|
|
# Set more balanced column distribution
|
|
|
|
|
header = self.track_table.horizontalHeader()
|
|
|
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Track - flexible
|
|
|
|
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Artist - flexible
|
|
|
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Retry Count - flexible
|
|
|
|
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Status - flexible
|
|
|
|
|
|
|
|
|
|
# Set minimum widths to ensure readability
|
|
|
|
|
self.track_table.setMinimumSize(800, 400)
|
|
|
|
|
header.setMinimumSectionSize(80) # Minimum width for any column
|
|
|
|
|
header.setDefaultSectionSize(150) # Default width for columns
|
|
|
|
|
|
|
|
|
|
# Enhanced table styling
|
|
|
|
|
self.track_table.setAlternatingRowColors(True)
|
|
|
|
|
self.track_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
|
|
|
self.track_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
|
|
|
self.track_table.verticalHeader().setVisible(False)
|
|
|
|
|
self.track_table.setShowGrid(False)
|
|
|
|
|
|
|
|
|
|
self.track_table.setStyleSheet("""
|
|
|
|
|
QTableWidget {
|
|
|
|
|
background: #282828;
|
|
|
|
|
border: 1px solid #404040;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
gridline-color: #404040;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
gridline-color: transparent;
|
|
|
|
|
outline: none;
|
|
|
|
|
font-family: "SF Pro Text";
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
QTableWidget::item {
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border-bottom: 1px solid #404040;
|
|
|
|
|
padding: 12px 8px;
|
|
|
|
|
border: none;
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
QTableWidget::item:alternate {
|
|
|
|
|
background: rgba(255, 255, 255, 0.02);
|
|
|
|
|
}
|
|
|
|
|
QTableWidget::item:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
}
|
|
|
|
|
QTableWidget::item:selected {
|
|
|
|
|
background: rgba(29, 185, 84, 0.1);
|
|
|
|
|
border-left: 2px solid #1db954;
|
|
|
|
|
}
|
|
|
|
|
QHeaderView::section {
|
|
|
|
|
background: #404040;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
|
|
|
stop: 0 rgba(255, 255, 255, 0.08),
|
|
|
|
|
stop: 1 rgba(255, 255, 255, 0.04));
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
padding: 12px 8px;
|
|
|
|
|
border: none;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
QHeaderView::section:first {
|
|
|
|
|
border-top-left-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
QHeaderView::section:last {
|
|
|
|
|
border-top-right-radius: 12px;
|
|
|
|
|
}
|
|
|
|
|
QScrollBar:vertical {
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
width: 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
QScrollBar::handle:vertical {
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
min-height: 20px;
|
|
|
|
|
}
|
|
|
|
|
QScrollBar::handle:vertical:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Progress bar
|
|
|
|
|
# Enhanced progress bar
|
|
|
|
|
self.download_progress = QProgressBar()
|
|
|
|
|
self.download_progress.setFixedHeight(8)
|
|
|
|
|
self.download_progress.setVisible(False)
|
|
|
|
|
self.download_progress.setTextVisible(False)
|
|
|
|
|
self.download_progress.setStyleSheet("""
|
|
|
|
|
QProgressBar {
|
|
|
|
|
border: 1px solid #404040;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
background: #282828;
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
QProgressBar::chunk {
|
|
|
|
|
background: #1db954;
|
|
|
|
|
border-radius: 7px;
|
|
|
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
|
|
|
|
|
stop: 0 #1db954,
|
|
|
|
|
stop: 1 #1ed760);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Buttons
|
|
|
|
|
# Enhanced buttons
|
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
|
|
button_layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
self.begin_download_btn = QPushButton("🚀 Begin Downloads")
|
|
|
|
|
self.begin_download_btn.setFixedHeight(40)
|
|
|
|
|
self.begin_download_btn.setFixedHeight(44)
|
|
|
|
|
self.begin_download_btn.setMinimumWidth(160)
|
|
|
|
|
self.begin_download_btn.clicked.connect(self.start_downloads)
|
|
|
|
|
self.begin_download_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background: #1db954;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
|
|
|
stop: 0 #1db954,
|
|
|
|
|
stop: 1 #1ed760);
|
|
|
|
|
border: 1px solid rgba(29, 185, 84, 0.3);
|
|
|
|
|
border-radius: 22px;
|
|
|
|
|
color: #000000;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
font-family: "SF Pro Text";
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
padding: 0px 24px;
|
|
|
|
|
letter-spacing: 0.2px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background: #1ed760;
|
|
|
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
|
|
|
stop: 0 #1ed760,
|
|
|
|
|
stop: 1 #22e968);
|
|
|
|
|
border: 1px solid rgba(30, 215, 96, 0.4);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
QPushButton:pressed {
|
|
|
|
|
background: #1db954;
|
|
|
|
|
transform: translateY(0px);
|
|
|
|
|
}
|
|
|
|
|
QPushButton:disabled {
|
|
|
|
|
background: #404040;
|
|
|
|
|
color: #888888;
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
self.cancel_btn = QPushButton("Cancel")
|
|
|
|
|
self.cancel_btn.setFixedHeight(40)
|
|
|
|
|
self.cancel_btn.clicked.connect(self.on_cancel_clicked)
|
|
|
|
|
self.cancel_btn.setStyleSheet("""
|
|
|
|
|
# Clear wishlist button
|
|
|
|
|
self.clear_wishlist_btn = QPushButton("🗑️ Clear All")
|
|
|
|
|
self.clear_wishlist_btn.setFixedHeight(44)
|
|
|
|
|
self.clear_wishlist_btn.setMinimumWidth(120)
|
|
|
|
|
self.clear_wishlist_btn.clicked.connect(self.clear_wishlist)
|
|
|
|
|
self.clear_wishlist_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background: #404040;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
background: rgba(255, 107, 107, 0.1);
|
|
|
|
|
border: 1px solid rgba(255, 107, 107, 0.2);
|
|
|
|
|
border-radius: 22px;
|
|
|
|
|
color: #ff6b6b;
|
|
|
|
|
font-family: "SF Pro Text";
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
padding: 0px 20px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background: #505050;
|
|
|
|
|
background: rgba(255, 107, 107, 0.15);
|
|
|
|
|
border: 1px solid rgba(255, 107, 107, 0.3);
|
|
|
|
|
}
|
|
|
|
|
QPushButton:pressed {
|
|
|
|
|
background: rgba(255, 107, 107, 0.05);
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
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("""
|
|
|
|
|
self.cancel_btn = QPushButton("Close")
|
|
|
|
|
self.cancel_btn.setFixedHeight(44)
|
|
|
|
|
self.cancel_btn.setMinimumWidth(100)
|
|
|
|
|
self.cancel_btn.clicked.connect(self.on_cancel_clicked)
|
|
|
|
|
self.cancel_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background: #e22134;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 22px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
font-family: "SF Pro Text";
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
padding: 0px 20px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background: #ff4757;
|
|
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
|
|
}
|
|
|
|
|
QPushButton:pressed {
|
|
|
|
|
background: rgba(255, 255, 255, 0.02);
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
button_layout.addStretch()
|
|
|
|
|
# Add buttons to layout
|
|
|
|
|
button_layout.addWidget(self.clear_wishlist_btn)
|
|
|
|
|
button_layout.addStretch()
|
|
|
|
|
button_layout.addWidget(self.cancel_btn)
|
|
|
|
|
button_layout.addWidget(self.begin_download_btn)
|
|
|
|
|
|
|
|
|
|
# Add to layout
|
|
|
|
|
layout.addWidget(header_label)
|
|
|
|
|
# Add all components to main layout
|
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
|
layout.addWidget(self.info_label)
|
|
|
|
|
layout.addWidget(self.track_table)
|
|
|
|
|
layout.addWidget(self.download_progress)
|
|
|
|
|
@ -211,8 +343,14 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
# Check if automatic processing is running and update UI accordingly
|
|
|
|
|
if hasattr(self.parent_dashboard, 'auto_processing_wishlist') and self.parent_dashboard.auto_processing_wishlist:
|
|
|
|
|
self.info_label.setText(f"Found {self.total_tracks} tracks in wishlist (⚡ Automatic processing in progress...)")
|
|
|
|
|
self.begin_download_btn.setText("⏳ Auto Processing...")
|
|
|
|
|
self.begin_download_btn.setEnabled(False)
|
|
|
|
|
else:
|
|
|
|
|
self.info_label.setText(f"Found {self.total_tracks} tracks in wishlist ready for retry")
|
|
|
|
|
|
|
|
|
|
# Populate table
|
|
|
|
|
self.track_table.setRowCount(self.total_tracks)
|
|
|
|
|
@ -237,12 +375,53 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
logger.error(f"Error loading wishlist tracks: {e}")
|
|
|
|
|
self.info_label.setText(f"Error loading tracks: {str(e)}")
|
|
|
|
|
|
|
|
|
|
def check_auto_processing_status(self):
|
|
|
|
|
"""Periodically check if automatic processing status has changed"""
|
|
|
|
|
try:
|
|
|
|
|
is_auto_processing = hasattr(self.parent_dashboard, 'auto_processing_wishlist') and self.parent_dashboard.auto_processing_wishlist
|
|
|
|
|
current_button_text = self.begin_download_btn.text()
|
|
|
|
|
|
|
|
|
|
if is_auto_processing and "Auto Processing" not in current_button_text:
|
|
|
|
|
# Auto processing started, update UI
|
|
|
|
|
self.info_label.setText(f"Found {self.total_tracks} tracks in wishlist (⚡ Automatic processing in progress...)")
|
|
|
|
|
self.begin_download_btn.setText("⏳ Auto Processing...")
|
|
|
|
|
self.begin_download_btn.setEnabled(False)
|
|
|
|
|
logger.debug("Modal UI updated: Automatic processing started")
|
|
|
|
|
|
|
|
|
|
elif not is_auto_processing and "Auto Processing" in current_button_text:
|
|
|
|
|
# Auto processing finished, refresh the modal
|
|
|
|
|
self.load_wishlist_tracks()
|
|
|
|
|
self.begin_download_btn.setText("🚀 Begin Downloads")
|
|
|
|
|
self.begin_download_btn.setEnabled(self.total_tracks > 0)
|
|
|
|
|
logger.debug("Modal UI updated: Automatic processing completed")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error checking auto processing status: {e}")
|
|
|
|
|
|
|
|
|
|
def refresh_if_auto_processing_complete(self):
|
|
|
|
|
"""Check if automatic processing completed and refresh the modal"""
|
|
|
|
|
try:
|
|
|
|
|
if not hasattr(self.parent_dashboard, 'auto_processing_wishlist') or not self.parent_dashboard.auto_processing_wishlist:
|
|
|
|
|
# Auto processing is not running, refresh the modal
|
|
|
|
|
self.load_wishlist_tracks()
|
|
|
|
|
self.begin_download_btn.setText("🚀 Begin Downloads")
|
|
|
|
|
self.begin_download_btn.setEnabled(self.total_tracks > 0)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error refreshing modal after auto processing: {e}")
|
|
|
|
|
|
|
|
|
|
def start_downloads(self):
|
|
|
|
|
"""Start downloading all wishlist tracks"""
|
|
|
|
|
try:
|
|
|
|
|
if self.total_tracks == 0:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Check if automatic processing is already running
|
|
|
|
|
if hasattr(self.parent_dashboard, 'auto_processing_wishlist') and self.parent_dashboard.auto_processing_wishlist:
|
|
|
|
|
QMessageBox.information(self, "Wishlist Processing",
|
|
|
|
|
"Automatic wishlist processing is currently running in the background. "
|
|
|
|
|
"Please wait for it to complete before starting manual processing.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.download_in_progress = True
|
|
|
|
|
self.cancel_requested = False
|
|
|
|
|
self.begin_download_btn.setEnabled(False)
|
|
|
|
|
@ -275,10 +454,7 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
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)
|
|
|
|
|
# Start search and download for this track (status updates handled by worker)
|
|
|
|
|
self.search_and_download_track_simple(track_data, self.download_queue_index)
|
|
|
|
|
|
|
|
|
|
self.active_parallel_downloads += 1
|
|
|
|
|
@ -304,8 +480,9 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
self.on_track_download_failed(download_index, "Cannot create search query")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Use a simple approach - directly call soulseek search
|
|
|
|
|
# Use enhanced worker with status updates
|
|
|
|
|
worker = SimpleWishlistDownloadWorker(self.soulseek_client, query, track_data, download_index)
|
|
|
|
|
worker.signals.status_updated.connect(self.on_track_status_updated)
|
|
|
|
|
worker.signals.download_completed.connect(self.on_track_download_completed)
|
|
|
|
|
worker.signals.download_failed.connect(self.on_track_download_failed)
|
|
|
|
|
|
|
|
|
|
@ -315,6 +492,15 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
logger.error(f"Error starting track download: {e}")
|
|
|
|
|
self.on_track_download_failed(download_index, str(e))
|
|
|
|
|
|
|
|
|
|
def on_track_status_updated(self, download_index, status_text):
|
|
|
|
|
"""Handle live status updates for individual tracks"""
|
|
|
|
|
try:
|
|
|
|
|
if 0 <= download_index < self.track_table.rowCount():
|
|
|
|
|
self.track_table.setItem(download_index, 3, QTableWidgetItem(status_text))
|
|
|
|
|
logger.debug(f"Updated track {download_index} status to: {status_text}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error updating track status: {e}")
|
|
|
|
|
|
|
|
|
|
def on_track_download_completed(self, download_index, download_id):
|
|
|
|
|
"""Handle successful track download"""
|
|
|
|
|
try:
|
|
|
|
|
@ -419,12 +605,29 @@ class DownloadMissingWishlistTracksModal(QDialog):
|
|
|
|
|
self.cancel_requested = True
|
|
|
|
|
self.process_finished.emit()
|
|
|
|
|
self.reject()
|
|
|
|
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
|
|
"""Clean up when modal is closed"""
|
|
|
|
|
try:
|
|
|
|
|
# Stop the status check timer
|
|
|
|
|
if hasattr(self, 'status_check_timer') and self.status_check_timer:
|
|
|
|
|
self.status_check_timer.stop()
|
|
|
|
|
|
|
|
|
|
# Cancel any ongoing downloads
|
|
|
|
|
if self.download_in_progress:
|
|
|
|
|
self.cancel_requested = True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error during modal close: {e}")
|
|
|
|
|
|
|
|
|
|
super().closeEvent(event)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleWishlistDownloadWorker(QRunnable):
|
|
|
|
|
"""Simple worker to download a single wishlist track"""
|
|
|
|
|
"""Enhanced worker to download a single wishlist track with detailed status updates"""
|
|
|
|
|
|
|
|
|
|
class Signals(QObject):
|
|
|
|
|
status_updated = pyqtSignal(int, str) # download_index, status_text
|
|
|
|
|
download_completed = pyqtSignal(int, str) # download_index, download_id
|
|
|
|
|
download_failed = pyqtSignal(int, str) # download_index, error_message
|
|
|
|
|
|
|
|
|
|
@ -437,8 +640,11 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
|
|
|
|
self.signals = self.Signals()
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
"""Run the download"""
|
|
|
|
|
"""Run the download with detailed status updates"""
|
|
|
|
|
try:
|
|
|
|
|
# Update status: Starting search
|
|
|
|
|
self.signals.status_updated.emit(self.download_index, "🔍 Searching...")
|
|
|
|
|
|
|
|
|
|
# Get quality preference
|
|
|
|
|
from config.settings import config_manager
|
|
|
|
|
quality_preference = config_manager.get_quality_preference()
|
|
|
|
|
@ -448,12 +654,33 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
download_id = loop.run_until_complete(
|
|
|
|
|
self.soulseek_client.search_and_download_best(self.query, quality_preference)
|
|
|
|
|
# Update status: Found candidates, analyzing
|
|
|
|
|
self.signals.status_updated.emit(self.download_index, "🔎 Analyzing results...")
|
|
|
|
|
|
|
|
|
|
# Use the enhanced search method that provides more feedback
|
|
|
|
|
results = loop.run_until_complete(
|
|
|
|
|
self._search_with_progress(self.query, quality_preference)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if download_id:
|
|
|
|
|
self.signals.download_completed.emit(self.download_index, download_id)
|
|
|
|
|
if results and len(results) > 0:
|
|
|
|
|
# Update status: Found candidates, starting download
|
|
|
|
|
self.signals.status_updated.emit(self.download_index, f"📋 Found {len(results)} candidates")
|
|
|
|
|
time.sleep(0.5) # Brief pause so user can see the status
|
|
|
|
|
|
|
|
|
|
# Get the best result and start download
|
|
|
|
|
best_result = results[0] # Assuming results are sorted by quality
|
|
|
|
|
|
|
|
|
|
self.signals.status_updated.emit(self.download_index, "⏬ Starting download...")
|
|
|
|
|
|
|
|
|
|
# Start the actual download
|
|
|
|
|
download_id = loop.run_until_complete(
|
|
|
|
|
self.soulseek_client.download_track(best_result)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if download_id:
|
|
|
|
|
self.signals.download_completed.emit(self.download_index, download_id)
|
|
|
|
|
else:
|
|
|
|
|
self.signals.download_failed.emit(self.download_index, "Download failed to start")
|
|
|
|
|
else:
|
|
|
|
|
self.signals.download_failed.emit(self.download_index, "No search results found")
|
|
|
|
|
|
|
|
|
|
@ -462,6 +689,39 @@ class SimpleWishlistDownloadWorker(QRunnable):
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.signals.download_failed.emit(self.download_index, str(e))
|
|
|
|
|
|
|
|
|
|
async def _search_with_progress(self, query, quality_preference):
|
|
|
|
|
"""Search for tracks with progress updates"""
|
|
|
|
|
try:
|
|
|
|
|
# Emit search progress
|
|
|
|
|
self.signals.status_updated.emit(self.download_index, "🌐 Searching network...")
|
|
|
|
|
|
|
|
|
|
# Perform the search (this would ideally use the soulseek client's search methods)
|
|
|
|
|
# For now, we'll use the existing search_and_download_best method
|
|
|
|
|
# but in a real implementation, you'd want to separate search from download
|
|
|
|
|
|
|
|
|
|
# This is a simplified version - in practice you'd want to:
|
|
|
|
|
# 1. Search for candidates
|
|
|
|
|
# 2. Filter by quality
|
|
|
|
|
# 3. Return the results for manual download
|
|
|
|
|
|
|
|
|
|
# For now, let's use a direct approach
|
|
|
|
|
from core.soulseek_client import SoulseekClient
|
|
|
|
|
if hasattr(self.soulseek_client, 'search_tracks'):
|
|
|
|
|
results = await self.soulseek_client.search_tracks(query)
|
|
|
|
|
|
|
|
|
|
if results:
|
|
|
|
|
# Filter by quality preference if needed
|
|
|
|
|
filtered_results = self.soulseek_client.filter_results_by_quality_preference(
|
|
|
|
|
results, quality_preference
|
|
|
|
|
)
|
|
|
|
|
return filtered_results
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error in search with progress: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MetadataUpdateWorker(QThread):
|
|
|
|
|
@ -1133,11 +1393,19 @@ class DashboardDataProvider(QObject):
|
|
|
|
|
print(f"DEBUG: Testing {service} connection")
|
|
|
|
|
print(f"DEBUG: Available service clients: {list(self.service_clients.keys())}")
|
|
|
|
|
|
|
|
|
|
if service not in self.service_clients:
|
|
|
|
|
print(f"DEBUG: Service {service} not found in service_clients")
|
|
|
|
|
# Map service names to client keys
|
|
|
|
|
service_key_map = {
|
|
|
|
|
'spotify': 'spotify_client',
|
|
|
|
|
'plex': 'plex_client',
|
|
|
|
|
'soulseek': 'soulseek_client'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client_key = service_key_map.get(service, service)
|
|
|
|
|
if client_key not in self.service_clients:
|
|
|
|
|
print(f"DEBUG: Service {service} (key: {client_key}) not found in service_clients")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"DEBUG: Service client for {service}: {self.service_clients[service]}")
|
|
|
|
|
print(f"DEBUG: Service client for {service}: {self.service_clients[client_key]}")
|
|
|
|
|
|
|
|
|
|
# Clean up any existing test thread for this service
|
|
|
|
|
if hasattr(self, '_test_threads') and service in self._test_threads:
|
|
|
|
|
@ -1152,7 +1420,7 @@ class DashboardDataProvider(QObject):
|
|
|
|
|
self._test_threads = {}
|
|
|
|
|
|
|
|
|
|
# Run connection test in background thread
|
|
|
|
|
test_thread = ServiceTestThread(service, self.service_clients[service])
|
|
|
|
|
test_thread = ServiceTestThread(service, self.service_clients[client_key])
|
|
|
|
|
test_thread.test_completed.connect(self.on_service_test_completed)
|
|
|
|
|
test_thread.finished.connect(lambda: self._cleanup_test_thread(service))
|
|
|
|
|
self._test_threads[service] = test_thread
|
|
|
|
|
@ -1662,8 +1930,9 @@ class DashboardPage(QWidget):
|
|
|
|
|
|
|
|
|
|
# Timer for automatic wishlist retry processing
|
|
|
|
|
self.wishlist_retry_timer = QTimer()
|
|
|
|
|
self.wishlist_retry_timer.setSingleShot(True) # Single shot timer, we'll restart it after each completion
|
|
|
|
|
self.wishlist_retry_timer.timeout.connect(self.process_wishlist_automatically)
|
|
|
|
|
self.wishlist_retry_timer.start(3600000) # Process every hour (3600000 ms)
|
|
|
|
|
self.wishlist_retry_timer.start(60000) # Start first processing 1 minute after app launch (60000 ms)
|
|
|
|
|
|
|
|
|
|
# Track if automatic processing is currently running
|
|
|
|
|
self.auto_processing_wishlist = False
|
|
|
|
|
@ -1995,7 +2264,12 @@ class DashboardPage(QWidget):
|
|
|
|
|
|
|
|
|
|
def start_database_update(self):
|
|
|
|
|
"""Start the SoulSync database update process"""
|
|
|
|
|
if not hasattr(self, 'data_provider') or not self.data_provider.service_clients.get('plex'):
|
|
|
|
|
logger.debug(f"Starting database update - data_provider exists: {hasattr(self, 'data_provider')}")
|
|
|
|
|
if hasattr(self, 'data_provider') and hasattr(self.data_provider, 'service_clients'):
|
|
|
|
|
logger.debug(f"Service clients available: {list(self.data_provider.service_clients.keys())}")
|
|
|
|
|
logger.debug(f"Plex client: {self.data_provider.service_clients.get('plex')}")
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, 'data_provider') or not self.data_provider.service_clients.get('plex_client'):
|
|
|
|
|
self.add_activity_item("❌", "Database Update", "Plex client not available", "Now")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
@ -2005,7 +2279,7 @@ class DashboardPage(QWidget):
|
|
|
|
|
|
|
|
|
|
# Start the database update worker
|
|
|
|
|
self.database_worker = DatabaseUpdateWorker(
|
|
|
|
|
self.data_provider.service_clients['plex'],
|
|
|
|
|
self.data_provider.service_clients['plex_client'],
|
|
|
|
|
"database/music_library.db",
|
|
|
|
|
full_refresh
|
|
|
|
|
)
|
|
|
|
|
@ -2157,11 +2431,15 @@ class DashboardPage(QWidget):
|
|
|
|
|
|
|
|
|
|
def start_metadata_update(self):
|
|
|
|
|
"""Start the Plex metadata update process"""
|
|
|
|
|
if not hasattr(self, 'data_provider') or not self.data_provider.service_clients.get('plex'):
|
|
|
|
|
logger.debug(f"Starting metadata update - data_provider exists: {hasattr(self, 'data_provider')}")
|
|
|
|
|
if hasattr(self, 'data_provider') and hasattr(self.data_provider, 'service_clients'):
|
|
|
|
|
logger.debug(f"Service clients available: {list(self.data_provider.service_clients.keys())}")
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, 'data_provider') or not self.data_provider.service_clients.get('plex_client'):
|
|
|
|
|
self.add_activity_item("❌", "Metadata Update", "Plex client not available", "Now")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not self.data_provider.service_clients.get('spotify'):
|
|
|
|
|
if not self.data_provider.service_clients.get('spotify_client'):
|
|
|
|
|
self.add_activity_item("❌", "Metadata Update", "Spotify client not available", "Now")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
@ -2172,8 +2450,8 @@ class DashboardPage(QWidget):
|
|
|
|
|
# Start the metadata update worker (it will handle artist retrieval in background)
|
|
|
|
|
self.metadata_worker = MetadataUpdateWorker(
|
|
|
|
|
None, # Artists will be loaded in the worker thread
|
|
|
|
|
self.data_provider.service_clients['plex'],
|
|
|
|
|
self.data_provider.service_clients['spotify'],
|
|
|
|
|
self.data_provider.service_clients['plex_client'],
|
|
|
|
|
self.data_provider.service_clients['spotify_client'],
|
|
|
|
|
refresh_interval_days
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@ -2404,6 +2682,53 @@ class DashboardPage(QWidget):
|
|
|
|
|
if hasattr(self, 'wishlist_retry_timer'):
|
|
|
|
|
self.wishlist_retry_timer.stop()
|
|
|
|
|
|
|
|
|
|
# Stop the data provider timers
|
|
|
|
|
if hasattr(self.data_provider, 'download_stats_timer'):
|
|
|
|
|
self.data_provider.download_stats_timer.stop()
|
|
|
|
|
if hasattr(self.data_provider, 'system_stats_timer'):
|
|
|
|
|
self.data_provider.system_stats_timer.stop()
|
|
|
|
|
|
|
|
|
|
# Clean up database-related threads and timers (only on actual shutdown)
|
|
|
|
|
if hasattr(self, 'database_worker') and self.database_worker and self.database_worker.isRunning():
|
|
|
|
|
try:
|
|
|
|
|
self.database_worker.stop()
|
|
|
|
|
self.database_worker.wait(2000) # Give it more time
|
|
|
|
|
if self.database_worker.isRunning():
|
|
|
|
|
self.database_worker.terminate()
|
|
|
|
|
self.database_worker.deleteLater()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug(f"Error cleaning up database worker: {e}")
|
|
|
|
|
|
|
|
|
|
if hasattr(self, 'database_stats_timer') and self.database_stats_timer:
|
|
|
|
|
try:
|
|
|
|
|
self.database_stats_timer.stop()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug(f"Error stopping database stats timer: {e}")
|
|
|
|
|
|
|
|
|
|
# Clean up any running stats workers
|
|
|
|
|
if hasattr(self, '_active_stats_workers') and self._active_stats_workers:
|
|
|
|
|
try:
|
|
|
|
|
for worker in self._active_stats_workers[:]: # Copy list to avoid modification issues
|
|
|
|
|
if worker and worker.isRunning():
|
|
|
|
|
worker.stop()
|
|
|
|
|
worker.wait(1000)
|
|
|
|
|
if worker:
|
|
|
|
|
worker.deleteLater()
|
|
|
|
|
self._active_stats_workers.clear()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug(f"Error cleaning up stats workers: {e}")
|
|
|
|
|
|
|
|
|
|
# Clean up metadata worker as well (only on shutdown)
|
|
|
|
|
if hasattr(self, 'metadata_worker') and self.metadata_worker and self.metadata_worker.isRunning():
|
|
|
|
|
try:
|
|
|
|
|
self.metadata_worker.stop()
|
|
|
|
|
self.metadata_worker.wait(2000) # Give it more time
|
|
|
|
|
if self.metadata_worker.isRunning():
|
|
|
|
|
self.metadata_worker.terminate()
|
|
|
|
|
self.metadata_worker.deleteLater()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug(f"Error cleaning up metadata worker: {e}")
|
|
|
|
|
|
|
|
|
|
super().closeEvent(event)
|
|
|
|
|
|
|
|
|
|
def cleanup_threads(self):
|
|
|
|
|
@ -2561,11 +2886,21 @@ class DashboardPage(QWidget):
|
|
|
|
|
# Update button count since tracks may have been removed
|
|
|
|
|
self.update_wishlist_button_count()
|
|
|
|
|
|
|
|
|
|
# Refresh any open wishlist modals
|
|
|
|
|
for widget in QApplication.instance().allWidgets():
|
|
|
|
|
if isinstance(widget, DownloadMissingWishlistTracksModal) and widget.isVisible():
|
|
|
|
|
widget.refresh_if_auto_processing_complete()
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
# Schedule next wishlist processing in 60 minutes
|
|
|
|
|
if hasattr(self, 'wishlist_retry_timer') and self.wishlist_retry_timer:
|
|
|
|
|
logger.info("Scheduling next automatic wishlist processing in 60 minutes")
|
|
|
|
|
self.wishlist_retry_timer.start(3600000) # 60 minutes (3600000 ms)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error handling automatic wishlist processing completion: {e}")
|
|
|
|
|
|
|
|
|
|
@ -2574,6 +2909,12 @@ class DashboardPage(QWidget):
|
|
|
|
|
try:
|
|
|
|
|
self.auto_processing_wishlist = False
|
|
|
|
|
logger.error(f"Automatic wishlist processing failed: {error_message}")
|
|
|
|
|
|
|
|
|
|
# Schedule next wishlist processing in 60 minutes even after error
|
|
|
|
|
if hasattr(self, 'wishlist_retry_timer') and self.wishlist_retry_timer:
|
|
|
|
|
logger.info("Scheduling next automatic wishlist processing in 60 minutes (after error)")
|
|
|
|
|
self.wishlist_retry_timer.start(3600000) # 60 minutes (3600000 ms)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error handling automatic wishlist processing error: {e}")
|
|
|
|
|
|
|
|
|
|
@ -2671,32 +3012,4 @@ class AutoWishlistProcessorWorker(QRunnable):
|
|
|
|
|
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'):
|
|
|
|
|
self.data_provider.download_stats_timer.stop()
|
|
|
|
|
if hasattr(self.data_provider, 'system_stats_timer'):
|
|
|
|
|
self.data_provider.system_stats_timer.stop()
|
|
|
|
|
|
|
|
|
|
# Clean up database-related threads and timers
|
|
|
|
|
if hasattr(self, 'database_worker') and self.database_worker.isRunning():
|
|
|
|
|
self.database_worker.stop()
|
|
|
|
|
self.database_worker.wait(1000)
|
|
|
|
|
self.database_worker.deleteLater()
|
|
|
|
|
|
|
|
|
|
if hasattr(self, 'database_stats_timer'):
|
|
|
|
|
self.database_stats_timer.stop()
|
|
|
|
|
|
|
|
|
|
# Clean up any running stats workers (keep track of them)
|
|
|
|
|
if hasattr(self, '_active_stats_workers'):
|
|
|
|
|
for worker in self._active_stats_workers:
|
|
|
|
|
if worker.isRunning():
|
|
|
|
|
worker.stop()
|
|
|
|
|
worker.wait(1000)
|
|
|
|
|
worker.deleteLater()
|
|
|
|
|
self._active_stats_workers.clear()
|
|
|
|
|
|
|
|
|
|
# Clean up metadata worker as well
|
|
|
|
|
if hasattr(self, 'metadata_worker') and self.metadata_worker.isRunning():
|
|
|
|
|
self.metadata_worker.stop()
|
|
|
|
|
self.metadata_worker.wait(1000)
|
|
|
|
|
self.metadata_worker.deleteLater()
|
|
|
|
|
# Worker is complete - no cleanup needed for this simple background task
|
|
|
|
|
|