This commit is contained in:
Broque Thomas 2025-07-25 21:26:41 -07:00
parent f138824c03
commit 347eccf762
5 changed files with 10398 additions and 326 deletions

View file

@ -1 +1 @@
{"access_token": "BQCCZ_QN3sFdtefCigaIuPuMYIdP2K25HRJ-NAQs1yLwHQkj9YxyvN3zAUEj6A3kANHI5yqH3DGN7oRzAO778PTcxQwcFFbpOWAKgi80owbwXVva_SwcjBL3MyLEAQJh2TN3iolRTWycnfHx0LSvT2Ddkm6o09xjQfxIfI14t5dEwummt4Ceqb0AykByuhtEpSCzjh6Z0cfTayvOYPv_1lgm2JSLalKSaz3osh9PhT-PRLAmbA-F1yCaTs73bnhc", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753495023, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"} {"access_token": "BQD6bPKD5klEqF4yItt47hgMBJ-wwIDl_zcqZhkU3IHgT8FD-qDv5QnKYFrnaBOogR4UvC0C-j23K7TfC06cKrIXNnwgV5M2uqh3gfCSF1joHk9pc0xeeIcLCTwP4Y9MeZ9yPiTAmvDal3Bo_tySvWmUdWuh8GJhx3c24Q1xx-vN3luBn_OP2jJpUBbJnF7IVnnd9JV7IFPcofYpl1SfXuOKLTDfo8CEsJ0FvCIeFSVVEJLjkaXYQOEnYDPGZIPI", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753507025, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}

10119
logs/app.log

File diff suppressed because it is too large Load diff

View file

@ -101,7 +101,7 @@ class MainWindow(QMainWindow):
self.dashboard_page = DashboardPage() self.dashboard_page = DashboardPage()
self.downloads_page = DownloadsPage(self.soulseek_client) self.downloads_page = DownloadsPage(self.soulseek_client)
self.sync_page = SyncPage(self.spotify_client, self.plex_client, self.soulseek_client, self.downloads_page) self.sync_page = SyncPage(self.spotify_client, self.plex_client, self.soulseek_client, self.downloads_page)
self.artists_page = ArtistsPage() self.artists_page = ArtistsPage(downloads_page=self.downloads_page)
self.settings_page = SettingsPage() self.settings_page = SettingsPage()
self.stacked_widget.addWidget(self.dashboard_page) self.stacked_widget.addWidget(self.dashboard_page)

View file

@ -16,6 +16,8 @@ from core.spotify_client import SpotifyClient, Artist, Album
from core.plex_client import PlexClient from core.plex_client import PlexClient
from core.soulseek_client import SoulseekClient, AlbumResult from core.soulseek_client import SoulseekClient, AlbumResult
from core.matching_engine import MusicMatchingEngine from core.matching_engine import MusicMatchingEngine
import asyncio
@dataclass @dataclass
class ArtistMatch: class ArtistMatch:
@ -24,6 +26,46 @@ class ArtistMatch:
confidence: float confidence: float
match_reason: str = "" match_reason: str = ""
class DownloadCompletionWorkerSignals(QObject):
"""Signals for the download completion worker"""
completed = pyqtSignal(object, str) # download_item, organized_path
error = pyqtSignal(object, str) # download_item, error_message
class DownloadCompletionWorker(QRunnable):
"""Background worker to handle download completion processing without blocking UI"""
def __init__(self, download_item, absolute_file_path, organize_func):
super().__init__()
self.download_item = download_item
self.absolute_file_path = absolute_file_path
self.organize_func = organize_func
self.signals = DownloadCompletionWorkerSignals()
def run(self):
"""Process download completion in background thread"""
try:
print(f"🧵 Background worker processing download...")
# Add a small delay to ensure file is fully written
import time
time.sleep(1)
# Organize the file into Transfer folder structure
organized_path = self.organize_func(self.download_item, self.absolute_file_path)
# Emit completion signal
self.signals.completed.emit(self.download_item, organized_path or self.absolute_file_path)
except Exception as e:
print(f"❌ Error in background worker: {e}")
import traceback
traceback.print_exc()
# Emit error signal
self.signals.error.emit(self.download_item, str(e))
class ImageDownloaderSignals(QObject): class ImageDownloaderSignals(QObject):
"""Signals for the ImageDownloader worker.""" """Signals for the ImageDownloader worker."""
finished = pyqtSignal(QLabel, QPixmap) finished = pyqtSignal(QLabel, QPixmap)
@ -146,9 +188,9 @@ class AlbumSearchWorker(QThread):
search_failed = pyqtSignal(str) search_failed = pyqtSignal(str)
search_progress = pyqtSignal(str) # Progress messages search_progress = pyqtSignal(str) # Progress messages
def __init__(self, album: Album, soulseek_client: SoulseekClient): def __init__(self, query: str, soulseek_client: SoulseekClient):
super().__init__() super().__init__()
self.album = album self.query = query
self.soulseek_client = soulseek_client self.soulseek_client = soulseek_client
self._stop_requested = False self._stop_requested = False
@ -157,38 +199,48 @@ class AlbumSearchWorker(QThread):
self._stop_requested = True self._stop_requested = True
def run(self): def run(self):
"""Executes the album search asynchronously."""
loop = None
try: try:
if not self.soulseek_client: if not self.soulseek_client:
self.search_failed.emit("Soulseek client not available") self.search_failed.emit("Soulseek client not available")
return return
# Create search query for the album # Create a new event loop for this thread to run async operations
search_query = f'"{self.album.name}"' loop = asyncio.new_event_loop()
if self.album.artists: asyncio.set_event_loop(loop)
search_query = f'"{self.album.artists[0]}" "{self.album.name}"'
self.search_progress.emit(f"Searching for: {search_query}") self.search_progress.emit(f"Searching for: {self.query}")
# Perform the search # Perform the async search using the provided query
results = self.soulseek_client.search(search_query) results = loop.run_until_complete(self.soulseek_client.search(self.query))
if self._stop_requested: if self._stop_requested:
return return
# Filter to album results only # The search method returns a tuple of (tracks, albums)
album_results = [] tracks, albums = results if results else ([], [])
if results: album_results = albums if albums else []
tracks, albums = results # Unpack the tuple returned by search
album_results = albums if albums else []
# Sort by quality/size # Sort by a combination of track count and total size for relevance
album_results.sort(key=lambda x: (x.total_size, x.track_count), reverse=True) album_results.sort(key=lambda x: (x.track_count, x.total_size), reverse=True)
self.search_results.emit(album_results) self.search_results.emit(album_results)
except Exception as e: except Exception as e:
if not self._stop_requested: if not self._stop_requested:
import traceback
traceback.print_exc()
self.search_failed.emit(str(e)) self.search_failed.emit(str(e))
finally:
# Ensure the event loop is properly closed
if loop:
try:
loop.close()
except Exception as e:
print(f"Error closing event loop in AlbumSearchWorker: {e}")
class PlexLibraryWorker(QThread): class PlexLibraryWorker(QThread):
"""Background worker for checking Plex library""" """Background worker for checking Plex library"""
@ -310,337 +362,284 @@ class AlbumSearchDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.album = album self.album = album
self.selected_album_result = None self.selected_album_result = None
self.selected_widget = None
self.search_worker = None self.search_worker = None
self.setup_ui() self.setup_ui()
self.start_search() self.start_search() # Start automatic search on open
def setup_ui(self): def setup_ui(self):
self.setWindowTitle(f"Download Album: {self.album.name}") self.setWindowTitle(f"Download Source for: {self.album.name}")
self.setFixedSize(600, 500) self.setFixedSize(800, 700)
self.setStyleSheet(""" self.setStyleSheet("""
QDialog { QDialog { background: #191414; color: #ffffff; }
background: #191414; QScrollArea { border: 1px solid #404040; border-radius: 8px; background: #282828; }
color: #ffffff; QLineEdit {
background: #333; border: 1px solid #555; border-radius: 4px;
padding: 8px; font-size: 12px;
} }
QPushButton {
background-color: #444; border: 1px solid #666; border-radius: 4px;
padding: 8px 12px; font-size: 12px;
}
QPushButton:hover { background-color: #555; }
""") """)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20) layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(16) layout.setSpacing(15)
# Header # Header
header_label = QLabel(f"Searching for: {self.album.name}") header_label = QLabel(f"Searching for: {self.album.name} by {', '.join(self.album.artists)}")
header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold)) header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold))
header_label.setStyleSheet("color: #ffffff; padding: 8px;")
artist_label = QLabel(f"By: {', '.join(self.album.artists)}") # Manual Search Section
artist_label.setFont(QFont("Arial", 11)) search_layout = QHBoxLayout()
artist_label.setStyleSheet("color: #b3b3b3; padding: 4px;") self.manual_search_input = QLineEdit()
self.manual_search_input.setPlaceholderText("Refine search: Artist Album Title...")
self.manual_search_input.returnPressed.connect(self.trigger_manual_search)
# Progress bar self.search_cancel_btn = QPushButton("Search")
self.progress_bar = QProgressBar() self.search_cancel_btn.setFixedWidth(120)
self.progress_bar.setRange(0, 0) # Indeterminate progress self.search_cancel_btn.clicked.connect(self.handle_search_cancel_click)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #404040;
border-radius: 4px;
background: #282828;
text-align: center;
color: #ffffff;
}
QProgressBar::chunk {
background: #1db954;
border-radius: 3px;
}
""")
# Status label search_layout.addWidget(self.manual_search_input, 1)
search_layout.addWidget(self.search_cancel_btn)
# Status
self.status_label = QLabel("Initializing search...") self.status_label = QLabel("Initializing search...")
self.status_label.setFont(QFont("Arial", 10)) self.status_label.setStyleSheet("color: #b3b3b3;")
self.status_label.setStyleSheet("color: #b3b3b3; padding: 4px;")
# Results area # Results Area
self.results_scroll = QScrollArea() self.results_scroll = QScrollArea()
self.results_scroll.setWidgetResizable(True) self.results_scroll.setWidgetResizable(True)
self.results_scroll.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 8px;
background: #282828;
}
QScrollBar:vertical {
background: #404040;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background: #1db954;
border-radius: 6px;
min-height: 20px;
}
""")
self.results_widget = QWidget() self.results_widget = QWidget()
self.results_layout = QVBoxLayout(self.results_widget) self.results_layout = QVBoxLayout(self.results_widget)
self.results_layout.setSpacing(8) self.results_layout.setSpacing(8)
self.results_layout.setContentsMargins(12, 12, 12, 12) self.results_layout.setContentsMargins(10, 10, 10, 10)
self.results_layout.addStretch(1)
self.results_scroll.setWidget(self.results_widget) self.results_scroll.setWidget(self.results_widget)
# Buttons # Bottom Buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
self.cancel_search_btn = QPushButton("Cancel Search")
self.cancel_search_btn.setStyleSheet("""
QPushButton {
background: #ff6b6b;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
padding: 8px 16px;
}
QPushButton:hover {
background: #ff5252;
}
""")
self.cancel_search_btn.clicked.connect(self.cancel_search)
self.download_btn = QPushButton("Download Selected") self.download_btn = QPushButton("Download Selected")
self.download_btn.setEnabled(False) self.download_btn.setEnabled(False) # Initially disabled
self.download_btn.setStyleSheet(""" self.download_btn.setStyleSheet("background-color: #1db954; color: black;")
QPushButton {
background: #1db954;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
padding: 8px 16px;
}
QPushButton:hover {
background: #1ed760;
}
QPushButton:disabled {
background: #404040;
color: #808080;
}
""")
self.download_btn.clicked.connect(self.download_selected)
close_btn = QPushButton("Close") close_btn = QPushButton("Close")
close_btn.setStyleSheet("""
QPushButton { self.download_btn.clicked.connect(self.download_selected)
background: #404040;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
padding: 8px 16px;
}
QPushButton:hover {
background: #505050;
}
""")
close_btn.clicked.connect(self.reject) close_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_search_btn) button_layout.addStretch(1)
button_layout.addStretch()
button_layout.addWidget(self.download_btn) button_layout.addWidget(self.download_btn)
button_layout.addWidget(close_btn) button_layout.addWidget(close_btn)
# Add to main layout
layout.addWidget(header_label) layout.addWidget(header_label)
layout.addWidget(artist_label) layout.addLayout(search_layout)
layout.addWidget(self.progress_bar)
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
layout.addWidget(self.results_scroll, 1) layout.addWidget(self.results_scroll, 1)
layout.addLayout(button_layout) layout.addLayout(button_layout)
def start_search(self): def handle_search_cancel_click(self):
"""Start the album search""" """Toggles between starting a search and cancelling an active one."""
# We need to get the soulseek client from the parent if self.search_worker and self.search_worker.isRunning():
self.cancel_search()
else:
self.trigger_manual_search()
def trigger_manual_search(self):
"""Starts a new search using the text from the manual search input."""
query = self.manual_search_input.text().strip()
if query:
self.start_search(query)
def start_search(self, query: Optional[str] = None):
"""
Starts the album search. If a query is provided, it's a manual search.
Otherwise, it constructs an automatic query.
"""
if self.search_worker and self.search_worker.isRunning():
self.search_worker.stop()
self.search_worker.wait()
self.clear_results()
self.download_btn.setEnabled(False)
self.status_label.setText("Searching...")
self.set_search_button_to_cancel(True)
if query is None:
artist_part = self.album.artists[0] if self.album.artists else ""
query = f"{artist_part} {self.album.name}".strip()
self.manual_search_input.setText(query)
parent_page = self.parent() parent_page = self.parent()
if hasattr(parent_page, 'soulseek_client') and parent_page.soulseek_client: if hasattr(parent_page, 'soulseek_client') and parent_page.soulseek_client:
self.search_worker = AlbumSearchWorker(self.album, parent_page.soulseek_client) self.search_worker = AlbumSearchWorker(query, parent_page.soulseek_client)
self.search_worker.search_results.connect(self.on_search_results) self.search_worker.search_results.connect(self.on_search_results)
self.search_worker.search_failed.connect(self.on_search_failed) self.search_worker.search_failed.connect(self.on_search_failed)
self.search_worker.search_progress.connect(self.on_search_progress) self.search_worker.search_progress.connect(self.on_search_progress)
self.search_worker.finished.connect(self.on_search_finished)
self.search_worker.start() self.search_worker.start()
else: else:
self.on_search_failed("Soulseek client not available") self.on_search_failed("Soulseek client not available")
def cancel_search(self): def cancel_search(self):
"""Cancel the current search""" if self.search_worker and self.search_worker.isRunning():
if self.search_worker:
self.search_worker.stop() self.search_worker.stop()
self.search_worker.terminate() self.status_label.setText("Search cancelled.")
self.search_worker.wait() self.set_search_button_to_cancel(False)
self.reject()
def on_search_progress(self, message): def on_search_progress(self, message):
"""Handle search progress updates"""
self.status_label.setText(message) self.status_label.setText(message)
def on_search_results(self, album_results): def on_search_results(self, album_results):
"""Handle search results""" self.set_search_button_to_cancel(False)
self.clear_results()
if not album_results: if not album_results:
self.status_label.setText("No albums found") self.status_label.setText("No albums found for this query.")
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
return return
self.status_label.setText(f"Found {len(album_results)} albums") self.status_label.setText(f"Found {len(album_results)} potential albums. Click one to select.")
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
# Display results for album_result in album_results[:25]: # Show top 25
for i, album_result in enumerate(album_results[:10]): # Show top 10 results result_item = self.create_result_item(album_result)
result_item = self.create_result_item(album_result, i) self.results_layout.insertWidget(self.results_layout.count() - 1, result_item)
self.results_layout.addWidget(result_item)
self.results_layout.addStretch()
self.cancel_search_btn.setText("Cancel")
def on_search_failed(self, error): def on_search_failed(self, error):
"""Handle search failure""" self.set_search_button_to_cancel(False)
self.status_label.setText(f"Search failed: {error}") self.status_label.setText(f"Search failed: {error}")
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
self.cancel_search_btn.setText("Close")
def on_search_finished(self): def create_result_item(self, album_result: AlbumResult):
"""Handle search completion""" """Creates a larger, more informative, and clickable result item widget."""
self.cancel_search_btn.setText("Close")
def create_result_item(self, album_result: AlbumResult, index: int):
"""Create a result item widget"""
item = QFrame() item = QFrame()
item.setFixedHeight(75) # Increased height for better readability
item.setCursor(Qt.CursorShape.PointingHandCursor)
item.setStyleSheet(""" item.setStyleSheet("""
QFrame { QFrame {
background: rgba(40, 40, 40, 0.8); background: rgba(40, 40, 40, 0.8);
border: 1px solid #606060; border: 1px solid #555;
border-radius: 8px; border-radius: 6px;
padding: 8px;
}
QFrame:hover {
background: rgba(50, 50, 50, 0.9);
border: 1px solid #1db954;
} }
""") """)
item.setCursor(Qt.CursorShape.PointingHandCursor) # Connect the click event for the whole frame
item.mousePressEvent = lambda event: self.select_result(album_result, item)
layout = QHBoxLayout(item) layout = QHBoxLayout(item)
layout.setContentsMargins(12, 8, 12, 8) layout.setContentsMargins(15, 10, 15, 10)
layout.setSpacing(12) layout.setSpacing(15)
# Album info
info_layout = QVBoxLayout() info_layout = QVBoxLayout()
info_layout.setSpacing(4)
title_label = QLabel(album_result.album_title) title_label = QLabel(f"{album_result.album_title} by {album_result.artist}")
title_label.setFont(QFont("Arial", 11, QFont.Weight.Bold)) title_label.setFont(QFont("Arial", 11, QFont.Weight.Bold))
title_label.setStyleSheet("color: #ffffff;")
details_label = QLabel(f"By: {album_result.artist}{album_result.track_count} tracks • {self.format_size(album_result.total_size)}") details_text = (f"{album_result.track_count} tracks | "
f"{self.format_size(album_result.total_size)} | "
f"Uploader: {album_result.username}")
details_label = QLabel(details_text)
details_label.setFont(QFont("Arial", 9)) details_label.setFont(QFont("Arial", 9))
details_label.setStyleSheet("color: #b3b3b3;") details_label.setStyleSheet("color: #b3b3b3;")
uploader_label = QLabel(f"Uploader: {album_result.username}")
uploader_label.setFont(QFont("Arial", 8))
uploader_label.setStyleSheet("color: #888888;")
info_layout.addWidget(title_label) info_layout.addWidget(title_label)
info_layout.addWidget(details_label) info_layout.addWidget(details_label)
info_layout.addWidget(uploader_label)
# Select button quality_badge = self.create_quality_badge(album_result)
select_btn = QPushButton("Select")
select_btn.setFixedWidth(80)
select_btn.setStyleSheet("""
QPushButton {
background: #1db954;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
padding: 6px 12px;
}
QPushButton:hover {
background: #1ed760;
}
""")
select_btn.clicked.connect(lambda: self.select_result(album_result))
layout.addLayout(info_layout) layout.addLayout(info_layout, 1)
layout.addStretch() layout.addWidget(quality_badge)
layout.addWidget(select_btn)
return item return item
def format_size(self, size_bytes): def create_quality_badge(self, album_result: AlbumResult):
"""Format file size in human readable format""" """Creates a styled badge for displaying audio quality."""
if size_bytes >= 1024 * 1024 * 1024: quality = album_result.dominant_quality.upper()
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
elif size_bytes >= 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} MB"
elif size_bytes >= 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes} B"
def select_result(self, album_result): # Safely calculate average bitrate from the album's tracks
"""Select an album result""" bitrate = 0
if hasattr(album_result, 'tracks') and album_result.tracks:
valid_bitrates = [
track.bitrate for track in album_result.tracks
if hasattr(track, 'bitrate') and track.bitrate
]
if valid_bitrates:
bitrate = sum(valid_bitrates) // len(valid_bitrates)
badge_text = quality
if quality == 'MP3' and bitrate > 0:
badge_text = f"MP3 {bitrate}k"
elif quality == 'VBR':
badge_text = "MP3 VBR"
badge = QLabel(badge_text)
badge.setFixedWidth(80)
badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
badge.setFont(QFont("Arial", 9, QFont.Weight.Bold))
if quality == 'FLAC':
style = "background-color: #4CAF50; color: white; border-radius: 4px; padding: 5px;"
elif bitrate >= 320:
style = "background-color: #2196F3; color: white; border-radius: 4px; padding: 5px;"
elif bitrate >= 192 or quality == 'VBR':
style = "background-color: #FFC107; color: black; border-radius: 4px; padding: 5px;"
else:
style = "background-color: #F44336; color: white; border-radius: 4px; padding: 5px;"
badge.setStyleSheet(style)
return badge
def clear_results(self):
"""Removes all result widgets from the layout, preserving the stretch item."""
self.selected_widget = None # Clear selection
# Iterate backwards to safely remove items while preserving the stretch
for i in reversed(range(self.results_layout.count())):
item = self.results_layout.itemAt(i)
if item.widget():
widget = item.widget()
widget.deleteLater()
def format_size(self, size_bytes):
if size_bytes >= 1024**3: return f"{size_bytes / 1024**3:.1f} GB"
if size_bytes >= 1024**2: return f"{size_bytes / 1024**2:.1f} MB"
return f"{size_bytes / 1024:.1f} KB"
def select_result(self, album_result, selected_item_widget):
"""Handles the selection of a result and provides visual feedback."""
self.selected_album_result = album_result self.selected_album_result = album_result
self.download_btn.setEnabled(True) self.download_btn.setEnabled(True)
# Visual feedback - highlight selected item # Deselect previous widget
for i in range(self.results_layout.count()): if self.selected_widget:
item = self.results_layout.itemAt(i) self.selected_widget.setStyleSheet("""
if item and item.widget(): QFrame { background: rgba(40, 40, 40, 0.8); border: 1px solid #555; border-radius: 6px; }
widget = item.widget() """)
if hasattr(widget, 'setStyleSheet'):
if widget.layout() and widget.layout().itemAt(2) and widget.layout().itemAt(2).widget():
btn = widget.layout().itemAt(2).widget()
if btn.text() == "Select":
widget.setStyleSheet("""
QFrame {
background: rgba(40, 40, 40, 0.8);
border: 1px solid #606060;
border-radius: 8px;
padding: 8px;
}
""")
btn.setText("Select")
# Highlight the selected item # Apply selected style to the new widget
sender = self.sender() selected_item_widget.setStyleSheet("""
if sender: QFrame { background: rgba(29, 185, 84, 0.2); border: 1px solid #1db954; border-radius: 6px; }
parent_frame = sender.parent() """)
if parent_frame: self.selected_widget = selected_item_widget
parent_frame.setStyleSheet("""
QFrame {
background: rgba(29, 185, 84, 0.3);
border: 2px solid #1db954;
border-radius: 8px;
padding: 8px;
}
""")
sender.setText("Selected ✓")
def download_selected(self): def download_selected(self):
"""Download the selected album"""
if self.selected_album_result: if self.selected_album_result:
self.album_selected.emit(self.selected_album_result) self.album_selected.emit(self.selected_album_result)
self.accept() self.accept()
def set_search_button_to_cancel(self, is_searching: bool):
"""Changes the search button's text and style."""
if is_searching:
self.search_cancel_btn.setText("Cancel Search")
self.search_cancel_btn.setStyleSheet("background-color: #F44336; color: white;")
else:
self.search_cancel_btn.setText("Search")
self.search_cancel_btn.setStyleSheet("background-color: #1db954; color: black;")
def closeEvent(self, event): def closeEvent(self, event):
"""Handle dialog close""" self.cancel_search()
if self.search_worker:
self.search_worker.stop()
self.search_worker.terminate()
self.search_worker.wait()
super().closeEvent(event) super().closeEvent(event)
class ArtistResultCard(QFrame): class ArtistResultCard(QFrame):
"""Card widget for displaying artist search results""" """Card widget for displaying artist search results"""
artist_selected = pyqtSignal(object) # Artist object artist_selected = pyqtSignal(object) # Artist object
@ -998,13 +997,14 @@ class AlbumCard(QFrame):
super().mousePressEvent(event) super().mousePressEvent(event)
class ArtistsPage(QWidget): class ArtistsPage(QWidget):
def __init__(self, parent=None): def __init__(self, downloads_page=None, parent=None):
super().__init__(parent) super().__init__(parent)
# Core clients # Core clients
self.spotify_client = None self.spotify_client = None
self.plex_client = None self.plex_client = None
self.soulseek_client = None self.soulseek_client = None
self.downloads_page = downloads_page # Store reference to DownloadsPage
self.matching_engine = MusicMatchingEngine() self.matching_engine = MusicMatchingEngine()
# State management # State management
@ -1500,7 +1500,6 @@ class ArtistsPage(QWidget):
col = 0 col = 0
row += 1 row += 1
def start_plex_library_check(self, albums): def start_plex_library_check(self, albums):
"""Start Plex library check in background""" """Start Plex library check in background"""
# Stop any existing Plex worker # Stop any existing Plex worker
@ -1568,81 +1567,32 @@ class ArtistsPage(QWidget):
"""Handle album fetch failure""" """Handle album fetch failure"""
self.albums_status.setText(f"Failed to load albums: {error}") self.albums_status.setText(f"Failed to load albums: {error}")
def on_album_download_requested(self, album: Album):
def on_album_download_requested(self, album): """Handle album download request from an AlbumCard."""
"""Handle album download request"""
print(f"Download requested for album: {album.name} by {', '.join(album.artists)}") print(f"Download requested for album: {album.name} by {', '.join(album.artists)}")
# Open the album search dialog # Store the album object that needs to be downloaded
self.album_to_download = album
# Open the album search dialog to find a source on Soulseek
dialog = AlbumSearchDialog(album, self) dialog = AlbumSearchDialog(album, self)
dialog.album_selected.connect(self.on_album_selected_for_download) dialog.album_selected.connect(self.on_album_selected_for_download)
dialog.exec() dialog.exec()
def on_album_selected_for_download(self, album_result: AlbumResult): def on_album_selected_for_download(self, album_result: AlbumResult):
"""Handle album selection from search dialog""" """
Handles album selection from the search dialog and delegates the
matched album download process to the main DownloadsPage.
"""
print(f"Selected album for download: {album_result.album_title} by {album_result.artist}") print(f"Selected album for download: {album_result.album_title} by {album_result.artist}")
# Start download process for the selected album if self.downloads_page:
self.start_album_download(album_result) # Delegate to the DownloadsPage to handle the matched download
# This will open the Spotify matching modal and add to the central queue
def start_album_download(self, album_result: AlbumResult): print("🚀 Delegating to DownloadsPage to start matched album download...")
"""Start downloading the selected album""" self.downloads_page.start_matched_album_download(album_result)
try: else:
if not self.soulseek_client: QMessageBox.critical(self, "Error", "Downloads page is not connected. Cannot start download.")
QMessageBox.warning(self, "Error", "Soulseek client not available!")
return
# Create download items for each track in the album
for track in album_result.tracks:
# Create a unique download ID
download_id = f"{album_result.username}_{track.filename}"
# Add to download queue (we need to integrate with the main app's download queue)
self.add_to_download_queue(
title=track.title or track.filename,
artist=track.artist or album_result.artist,
album=album_result.album_title,
username=album_result.username,
filename=track.filename,
file_size=track.size,
download_id=download_id,
track_result=track
)
# Show confirmation
QMessageBox.information(
self,
"Download Started",
f"Started downloading {len(album_result.tracks)} tracks from '{album_result.album_title}'"
)
except Exception as e:
print(f"Error starting album download: {e}")
QMessageBox.critical(self, "Download Error", f"Failed to start download: {str(e)}")
def add_to_download_queue(self, title, artist, album, username, filename, file_size, download_id, track_result):
"""Add a track to the download queue - needs integration with main app"""
# TODO: This needs to integrate with the main application's download system
# For now, we'll simulate starting the download
print(f"Adding to download queue: {title} by {artist}")
# We need to access the main app's download manager
# This would typically be done by emitting a signal that the main app listens to
# or by accessing the download manager through a parent reference
# For now, just start the individual track download
if self.soulseek_client:
try:
# Start the download using the soulseek client
self.soulseek_client.download(
username=username,
filename=filename,
download_id=download_id
)
print(f"Started download: {filename}")
except Exception as e:
print(f"Failed to start download for {filename}: {e}")
def return_to_search(self): def return_to_search(self):
"""Return to search interface""" """Return to search interface"""
@ -1700,3 +1650,6 @@ class ArtistsPage(QWidget):
"""Handle page close/cleanup""" """Handle page close/cleanup"""
self.stop_all_workers() self.stop_all_workers()
super().closeEvent(event) super().closeEvent(event)