good
This commit is contained in:
parent
f138824c03
commit
347eccf762
5 changed files with 10398 additions and 326 deletions
|
|
@ -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
10119
logs/app.log
File diff suppressed because it is too large
Load diff
2
main.py
2
main.py
|
|
@ -101,7 +101,7 @@ class MainWindow(QMainWindow):
|
|||
self.dashboard_page = DashboardPage()
|
||||
self.downloads_page = DownloadsPage(self.soulseek_client)
|
||||
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.stacked_widget.addWidget(self.dashboard_page)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -16,6 +16,8 @@ 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
|
||||
import asyncio
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArtistMatch:
|
||||
|
|
@ -24,6 +26,46 @@ class ArtistMatch:
|
|||
confidence: float
|
||||
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):
|
||||
"""Signals for the ImageDownloader worker."""
|
||||
finished = pyqtSignal(QLabel, QPixmap)
|
||||
|
|
@ -146,9 +188,9 @@ class AlbumSearchWorker(QThread):
|
|||
search_failed = pyqtSignal(str)
|
||||
search_progress = pyqtSignal(str) # Progress messages
|
||||
|
||||
def __init__(self, album: Album, soulseek_client: SoulseekClient):
|
||||
def __init__(self, query: str, soulseek_client: SoulseekClient):
|
||||
super().__init__()
|
||||
self.album = album
|
||||
self.query = query
|
||||
self.soulseek_client = soulseek_client
|
||||
self._stop_requested = False
|
||||
|
||||
|
|
@ -157,38 +199,48 @@ class AlbumSearchWorker(QThread):
|
|||
self._stop_requested = True
|
||||
|
||||
def run(self):
|
||||
"""Executes the album search asynchronously."""
|
||||
loop = None
|
||||
try:
|
||||
if not self.soulseek_client:
|
||||
self.search_failed.emit("Soulseek client not available")
|
||||
return
|
||||
|
||||
# Create search query for the album
|
||||
search_query = f'"{self.album.name}"'
|
||||
if self.album.artists:
|
||||
search_query = f'"{self.album.artists[0]}" "{self.album.name}"'
|
||||
# Create a new event loop for this thread to run async operations
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
self.search_progress.emit(f"Searching for: {search_query}")
|
||||
self.search_progress.emit(f"Searching for: {self.query}")
|
||||
|
||||
# Perform the search
|
||||
results = self.soulseek_client.search(search_query)
|
||||
# Perform the async search using the provided query
|
||||
results = loop.run_until_complete(self.soulseek_client.search(self.query))
|
||||
|
||||
if self._stop_requested:
|
||||
return
|
||||
|
||||
# Filter to album results only
|
||||
album_results = []
|
||||
if results:
|
||||
tracks, albums = results # Unpack the tuple returned by search
|
||||
album_results = albums if albums else []
|
||||
# The search method returns a tuple of (tracks, albums)
|
||||
tracks, albums = results if results else ([], [])
|
||||
album_results = albums if albums else []
|
||||
|
||||
# Sort by quality/size
|
||||
album_results.sort(key=lambda x: (x.total_size, x.track_count), reverse=True)
|
||||
# Sort by a combination of track count and total size for relevance
|
||||
album_results.sort(key=lambda x: (x.track_count, x.total_size), reverse=True)
|
||||
|
||||
self.search_results.emit(album_results)
|
||||
|
||||
except Exception as e:
|
||||
if not self._stop_requested:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
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):
|
||||
"""Background worker for checking Plex library"""
|
||||
|
|
@ -310,337 +362,284 @@ class AlbumSearchDialog(QDialog):
|
|||
super().__init__(parent)
|
||||
self.album = album
|
||||
self.selected_album_result = None
|
||||
self.selected_widget = None
|
||||
self.search_worker = None
|
||||
self.setup_ui()
|
||||
self.start_search()
|
||||
self.start_search() # Start automatic search on open
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle(f"Download Album: {self.album.name}")
|
||||
self.setFixedSize(600, 500)
|
||||
self.setWindowTitle(f"Download Source for: {self.album.name}")
|
||||
self.setFixedSize(800, 700)
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background: #191414;
|
||||
color: #ffffff;
|
||||
QDialog { background: #191414; color: #ffffff; }
|
||||
QScrollArea { border: 1px solid #404040; border-radius: 8px; background: #282828; }
|
||||
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.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(16)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 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.setStyleSheet("color: #ffffff; padding: 8px;")
|
||||
|
||||
artist_label = QLabel(f"By: {', '.join(self.album.artists)}")
|
||||
artist_label.setFont(QFont("Arial", 11))
|
||||
artist_label.setStyleSheet("color: #b3b3b3; padding: 4px;")
|
||||
# Manual Search Section
|
||||
search_layout = QHBoxLayout()
|
||||
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.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
||||
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;
|
||||
}
|
||||
""")
|
||||
self.search_cancel_btn = QPushButton("Search")
|
||||
self.search_cancel_btn.setFixedWidth(120)
|
||||
self.search_cancel_btn.clicked.connect(self.handle_search_cancel_click)
|
||||
|
||||
# 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.setFont(QFont("Arial", 10))
|
||||
self.status_label.setStyleSheet("color: #b3b3b3; padding: 4px;")
|
||||
self.status_label.setStyleSheet("color: #b3b3b3;")
|
||||
|
||||
# Results area
|
||||
# Results Area
|
||||
self.results_scroll = QScrollArea()
|
||||
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_layout = QVBoxLayout(self.results_widget)
|
||||
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)
|
||||
|
||||
# Buttons
|
||||
# Bottom Buttons
|
||||
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.setEnabled(False)
|
||||
self.download_btn.setStyleSheet("""
|
||||
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)
|
||||
self.download_btn.setEnabled(False) # Initially disabled
|
||||
self.download_btn.setStyleSheet("background-color: #1db954; color: black;")
|
||||
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #404040;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #505050;
|
||||
}
|
||||
""")
|
||||
|
||||
self.download_btn.clicked.connect(self.download_selected)
|
||||
close_btn.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addWidget(self.cancel_search_btn)
|
||||
button_layout.addStretch()
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.download_btn)
|
||||
button_layout.addWidget(close_btn)
|
||||
|
||||
# Add to main layout
|
||||
layout.addWidget(header_label)
|
||||
layout.addWidget(artist_label)
|
||||
layout.addWidget(self.progress_bar)
|
||||
layout.addLayout(search_layout)
|
||||
layout.addWidget(self.status_label)
|
||||
layout.addWidget(self.results_scroll, 1)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def start_search(self):
|
||||
"""Start the album search"""
|
||||
# We need to get the soulseek client from the parent
|
||||
|
||||
def handle_search_cancel_click(self):
|
||||
"""Toggles between starting a search and cancelling an active one."""
|
||||
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()
|
||||
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_failed.connect(self.on_search_failed)
|
||||
self.search_worker.search_progress.connect(self.on_search_progress)
|
||||
self.search_worker.finished.connect(self.on_search_finished)
|
||||
self.search_worker.start()
|
||||
else:
|
||||
self.on_search_failed("Soulseek client not available")
|
||||
|
||||
|
||||
def cancel_search(self):
|
||||
"""Cancel the current search"""
|
||||
if self.search_worker:
|
||||
if self.search_worker and self.search_worker.isRunning():
|
||||
self.search_worker.stop()
|
||||
self.search_worker.terminate()
|
||||
self.search_worker.wait()
|
||||
self.reject()
|
||||
self.status_label.setText("Search cancelled.")
|
||||
self.set_search_button_to_cancel(False)
|
||||
|
||||
def on_search_progress(self, message):
|
||||
"""Handle search progress updates"""
|
||||
self.status_label.setText(message)
|
||||
|
||||
def on_search_results(self, album_results):
|
||||
"""Handle search results"""
|
||||
self.set_search_button_to_cancel(False)
|
||||
self.clear_results()
|
||||
if not album_results:
|
||||
self.status_label.setText("No albums found")
|
||||
self.progress_bar.setRange(0, 1)
|
||||
self.progress_bar.setValue(1)
|
||||
self.status_label.setText("No albums found for this query.")
|
||||
return
|
||||
|
||||
self.status_label.setText(f"Found {len(album_results)} potential albums. Click one to select.")
|
||||
|
||||
self.status_label.setText(f"Found {len(album_results)} albums")
|
||||
self.progress_bar.setRange(0, 1)
|
||||
self.progress_bar.setValue(1)
|
||||
|
||||
# Display results
|
||||
for i, album_result in enumerate(album_results[:10]): # Show top 10 results
|
||||
result_item = self.create_result_item(album_result, i)
|
||||
self.results_layout.addWidget(result_item)
|
||||
|
||||
self.results_layout.addStretch()
|
||||
self.cancel_search_btn.setText("Cancel")
|
||||
|
||||
for album_result in album_results[:25]: # Show top 25
|
||||
result_item = self.create_result_item(album_result)
|
||||
self.results_layout.insertWidget(self.results_layout.count() - 1, result_item)
|
||||
|
||||
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.progress_bar.setRange(0, 1)
|
||||
self.progress_bar.setValue(1)
|
||||
self.cancel_search_btn.setText("Close")
|
||||
|
||||
def on_search_finished(self):
|
||||
"""Handle search completion"""
|
||||
self.cancel_search_btn.setText("Close")
|
||||
|
||||
def create_result_item(self, album_result: AlbumResult, index: int):
|
||||
"""Create a result item widget"""
|
||||
|
||||
def create_result_item(self, album_result: AlbumResult):
|
||||
"""Creates a larger, more informative, and clickable result item widget."""
|
||||
item = QFrame()
|
||||
item.setFixedHeight(75) # Increased height for better readability
|
||||
item.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
item.setStyleSheet("""
|
||||
QFrame {
|
||||
background: rgba(40, 40, 40, 0.8);
|
||||
border: 1px solid #606060;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
QFrame:hover {
|
||||
background: rgba(50, 50, 50, 0.9);
|
||||
border: 1px solid #1db954;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
}
|
||||
""")
|
||||
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.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(12)
|
||||
layout.setContentsMargins(15, 10, 15, 10)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Album info
|
||||
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.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.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(details_label)
|
||||
info_layout.addWidget(uploader_label)
|
||||
|
||||
# Select button
|
||||
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))
|
||||
quality_badge = self.create_quality_badge(album_result)
|
||||
|
||||
layout.addLayout(info_layout)
|
||||
layout.addStretch()
|
||||
layout.addWidget(select_btn)
|
||||
layout.addLayout(info_layout, 1)
|
||||
layout.addWidget(quality_badge)
|
||||
|
||||
return item
|
||||
|
||||
def format_size(self, size_bytes):
|
||||
"""Format file size in human readable format"""
|
||||
if size_bytes >= 1024 * 1024 * 1024:
|
||||
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"
|
||||
|
||||
def create_quality_badge(self, album_result: AlbumResult):
|
||||
"""Creates a styled badge for displaying audio quality."""
|
||||
quality = album_result.dominant_quality.upper()
|
||||
|
||||
# Safely calculate average bitrate from the album's tracks
|
||||
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:
|
||||
return f"{size_bytes} B"
|
||||
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):
|
||||
"""Select an album result"""
|
||||
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.download_btn.setEnabled(True)
|
||||
|
||||
# Deselect previous widget
|
||||
if self.selected_widget:
|
||||
self.selected_widget.setStyleSheet("""
|
||||
QFrame { background: rgba(40, 40, 40, 0.8); border: 1px solid #555; border-radius: 6px; }
|
||||
""")
|
||||
|
||||
# Visual feedback - highlight selected item
|
||||
for i in range(self.results_layout.count()):
|
||||
item = self.results_layout.itemAt(i)
|
||||
if item and item.widget():
|
||||
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
|
||||
sender = self.sender()
|
||||
if sender:
|
||||
parent_frame = sender.parent()
|
||||
if parent_frame:
|
||||
parent_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background: rgba(29, 185, 84, 0.3);
|
||||
border: 2px solid #1db954;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
""")
|
||||
sender.setText("Selected ✓")
|
||||
|
||||
# Apply selected style to the new widget
|
||||
selected_item_widget.setStyleSheet("""
|
||||
QFrame { background: rgba(29, 185, 84, 0.2); border: 1px solid #1db954; border-radius: 6px; }
|
||||
""")
|
||||
self.selected_widget = selected_item_widget
|
||||
|
||||
def download_selected(self):
|
||||
"""Download the selected album"""
|
||||
if self.selected_album_result:
|
||||
self.album_selected.emit(self.selected_album_result)
|
||||
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):
|
||||
"""Handle dialog close"""
|
||||
if self.search_worker:
|
||||
self.search_worker.stop()
|
||||
self.search_worker.terminate()
|
||||
self.search_worker.wait()
|
||||
self.cancel_search()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
|
||||
class ArtistResultCard(QFrame):
|
||||
"""Card widget for displaying artist search results"""
|
||||
artist_selected = pyqtSignal(object) # Artist object
|
||||
|
|
@ -998,13 +997,14 @@ class AlbumCard(QFrame):
|
|||
super().mousePressEvent(event)
|
||||
|
||||
class ArtistsPage(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, downloads_page=None, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Core clients
|
||||
self.spotify_client = None
|
||||
self.plex_client = None
|
||||
self.soulseek_client = None
|
||||
self.downloads_page = downloads_page # Store reference to DownloadsPage
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
|
||||
# State management
|
||||
|
|
@ -1500,7 +1500,6 @@ class ArtistsPage(QWidget):
|
|||
col = 0
|
||||
row += 1
|
||||
|
||||
|
||||
def start_plex_library_check(self, albums):
|
||||
"""Start Plex library check in background"""
|
||||
# Stop any existing Plex worker
|
||||
|
|
@ -1568,82 +1567,33 @@ class ArtistsPage(QWidget):
|
|||
"""Handle album fetch failure"""
|
||||
self.albums_status.setText(f"Failed to load albums: {error}")
|
||||
|
||||
|
||||
def on_album_download_requested(self, album):
|
||||
"""Handle album download request"""
|
||||
def on_album_download_requested(self, album: Album):
|
||||
"""Handle album download request from an AlbumCard."""
|
||||
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.album_selected.connect(self.on_album_selected_for_download)
|
||||
dialog.exec()
|
||||
|
||||
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}")
|
||||
|
||||
# Start download process for the selected album
|
||||
self.start_album_download(album_result)
|
||||
|
||||
def start_album_download(self, album_result: AlbumResult):
|
||||
"""Start downloading the selected album"""
|
||||
try:
|
||||
if not self.soulseek_client:
|
||||
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}")
|
||||
|
||||
if self.downloads_page:
|
||||
# Delegate to the DownloadsPage to handle the matched download
|
||||
# This will open the Spotify matching modal and add to the central queue
|
||||
print("🚀 Delegating to DownloadsPage to start matched album download...")
|
||||
self.downloads_page.start_matched_album_download(album_result)
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", "Downloads page is not connected. Cannot start download.")
|
||||
|
||||
def return_to_search(self):
|
||||
"""Return to search interface"""
|
||||
# Stop any running workers
|
||||
|
|
@ -1699,4 +1649,7 @@ class ArtistsPage(QWidget):
|
|||
def closeEvent(self, event):
|
||||
"""Handle page close/cleanup"""
|
||||
self.stop_all_workers()
|
||||
super().closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue