This commit is contained in:
Broque Thomas 2025-07-21 12:09:27 -07:00
parent c62fb159de
commit cd45ba0b84
2 changed files with 294 additions and 21 deletions

View file

@ -203,23 +203,23 @@ Playlist Track → Plex Check → (Missing) → Soulseek Search → Quality Filt
- ✅ Background worker with progress tracking and confidence scoring
- ✅ String normalization, similarity scoring, and duration matching
2. **🔄 IN PROGRESS - Enhanced Modal Interface**
- Replace simple QMessageBox with sophisticated modal
- Modal closes sync window and opens new interface
- Dashboard with live counters: Total Tracks, Matched Tracks, To Download
- Enhanced track table with Matched and Downloaded status columns
- Dual progress bar system (Plex analysis + Download progress)
- Three-button system: Begin Search, Cancel, Close
2. **✅ COMPLETED - Enhanced Modal Interface**
- Replace simple QMessageBox with sophisticated modal
- Modal closes sync window and opens new interface
- Dashboard with live counters: Total Tracks, Matched Tracks, To Download
- Enhanced track table with Matched and Downloaded status columns
- Dual progress bar system (Plex analysis + Download progress)
- Three-button system: Begin Search, Cancel, Close
3. **⏳ PENDING - Modal State Persistence**
- Progress bubble system when modal is closed during operations
- Clickable bubble to reopen modal and review progress
- Maintain operation state across modal open/close cycles
3. **✅ COMPLETED - Modal State Persistence**
- Progress bubble system when modal is closed during operations
- Clickable bubble to reopen modal and review progress
- Maintain operation state across modal open/close cycles
4. **⏳ PENDING - Soulseek Search Integration**
- Implement per-track search strategy (track name → artist + track name)
- Leverage existing search filtering and quality selection
- Use async operations for performance
4. **✅ COMPLETED - Soulseek Search Integration**
- Implement per-track search strategy (track name → artist + track name)
- Leverage existing search filtering and quality selection
- Use async operations for performance
5. **⏳ PENDING - Download Queue Integration**
- Extend downloads.py with minimal custom path support

View file

@ -156,6 +156,73 @@ class PlaylistTrackAnalysisWorker(QRunnable):
print(f"Error calculating track confidence: {e}")
return 0.0
class TrackDownloadWorkerSignals(QObject):
"""Signals for track download worker"""
download_completed = pyqtSignal(int, int, str) # download_index, track_index, download_id
download_failed = pyqtSignal(int, int, str) # download_index, track_index, error_message
class TrackDownloadWorker(QRunnable):
"""Background worker to download individual tracks via Soulseek"""
def __init__(self, spotify_track, soulseek_client, download_index, track_index):
super().__init__()
self.spotify_track = spotify_track
self.soulseek_client = soulseek_client
self.download_index = download_index
self.track_index = track_index
self.signals = TrackDownloadWorkerSignals()
self._cancelled = False
def cancel(self):
"""Cancel the download operation"""
self._cancelled = True
def run(self):
"""Download the track via Soulseek"""
try:
if self._cancelled or not self.soulseek_client:
return
# Create search queries - try track name first, then artist + track
track_name = self.spotify_track.name
artist_name = self.spotify_track.artists[0] if self.spotify_track.artists else ""
search_queries = []
search_queries.append(track_name) # Try track name only first
if artist_name:
search_queries.append(f"{artist_name} {track_name}") # Then artist + track
download_id = None
# Try each search query until we find a download
for query in search_queries:
if self._cancelled:
return
print(f"🔍 Searching Soulseek: {query}")
# Use the async method (need to run in sync context)
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
download_id = loop.run_until_complete(
self.soulseek_client.search_and_download_best(query)
)
if download_id:
break # Success - stop trying other queries
finally:
loop.close()
if download_id:
self.signals.download_completed.emit(self.download_index, self.track_index, download_id)
else:
self.signals.download_failed.emit(self.download_index, self.track_index, "No search results found")
except Exception as e:
self.signals.download_failed.emit(self.download_index, self.track_index, str(e))
class PlaylistLoaderThread(QThread):
playlist_loaded = pyqtSignal(object) # Single playlist
loading_finished = pyqtSignal(int) # Total count
@ -1572,6 +1639,78 @@ class SyncPage(QWidget):
continue # Keep the stretch spacer
else:
self.playlist_layout.removeItem(item)
def create_download_progress_bubble(self, progress_info):
"""Create a progress bubble for ongoing downloads"""
if hasattr(self, 'progress_bubble'):
# Remove existing bubble
self.progress_bubble.deleteLater()
# Create bubble widget
self.progress_bubble = QFrame(self)
self.progress_bubble.setFixedSize(300, 80)
self.progress_bubble.setStyleSheet("""
QFrame {
background-color: #1db954;
border: 2px solid #169441;
border-radius: 15px;
color: #000000;
}
QLabel {
color: #000000;
font-weight: bold;
}
""")
# Create bubble layout
bubble_layout = QVBoxLayout(self.progress_bubble)
bubble_layout.setContentsMargins(10, 8, 10, 8)
bubble_layout.setSpacing(5)
# Title
title = QLabel(f"📥 {progress_info['playlist_name']}")
title.setFont(QFont("Arial", 11, QFont.Weight.Bold))
# Progress text
if progress_info['analysis_complete']:
progress_text = f"Downloading: {progress_info['download_progress']}/{progress_info['download_total']}"
else:
progress_text = f"Analyzing: {progress_info['analysis_progress']}/{progress_info['total_tracks']}"
progress_label = QLabel(progress_text)
progress_label.setFont(QFont("Arial", 9))
# Click to reopen
click_label = QLabel("Click to view details")
click_label.setFont(QFont("Arial", 8))
click_label.setStyleSheet("color: #666666;")
bubble_layout.addWidget(title)
bubble_layout.addWidget(progress_label)
bubble_layout.addWidget(click_label)
# Position bubble in top-right corner
self.progress_bubble.move(self.width() - 320, 20)
self.progress_bubble.show()
# Make bubble clickable
self.progress_bubble.mousePressEvent = lambda event: self.reopen_download_modal(progress_info['modal_reference'])
# Store reference for updates
self.progress_bubble.progress_info = progress_info
def reopen_download_modal(self, modal_reference):
"""Reopen the download modal from the progress bubble"""
if modal_reference and not modal_reference.isVisible():
# Remove bubble
if hasattr(self, 'progress_bubble'):
self.progress_bubble.deleteLater()
delattr(self, 'progress_bubble')
# Show modal
modal_reference.show()
modal_reference.activateWindow()
modal_reference.raise_()
class DownloadMissingTracksModal(QDialog):
@ -2305,7 +2444,7 @@ class DownloadMissingTracksModal(QDialog):
self.analysis_progress.setVisible(False)
def start_download_progress(self):
"""Start download progress tracking (simulated for now)"""
"""Start actual download progress tracking"""
print(f"🚀 Starting download progress for {len(self.missing_tracks)} tracks")
# Show download progress bar
@ -2313,10 +2452,140 @@ class DownloadMissingTracksModal(QDialog):
self.download_progress.setMaximum(len(self.missing_tracks))
self.download_progress.setValue(0)
# TODO: Implement actual download integration
# For now, simulate download progress
self.simulate_downloads()
# Start real downloads
self.start_soulseek_downloads()
def start_soulseek_downloads(self):
"""Start real Soulseek downloads for missing tracks"""
if not self.missing_tracks:
return
# Get Soulseek client from parent
soulseek_client = getattr(self.parent_page, 'soulseek_client', None)
if not soulseek_client:
QMessageBox.critical(self, "Soulseek Unavailable",
"Soulseek client not available. Please check your configuration.")
return
# Create download worker
self.download_in_progress = True
self.current_download = 0
# Start downloading first track
self.download_next_track()
def download_next_track(self):
"""Download the next missing track"""
if self.current_download >= len(self.missing_tracks):
# All downloads complete
self.on_all_downloads_complete()
return
track_result = self.missing_tracks[self.current_download]
track = track_result.spotify_track
track_index = self.find_track_index(track)
print(f"🎵 Downloading track {self.current_download + 1}/{len(self.missing_tracks)}: {track.name}")
# Update table to show downloading status
if track_index is not None:
downloading_item = QTableWidgetItem("⏬ Downloading")
downloading_item.setFlags(downloading_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
downloading_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(track_index, 4, downloading_item)
# Create download worker
soulseek_client = getattr(self.parent_page, 'soulseek_client', None)
worker = TrackDownloadWorker(track, soulseek_client, self.current_download, track_index)
worker.signals.download_completed.connect(self.on_track_download_complete)
worker.signals.download_failed.connect(self.on_track_download_failed)
# Track worker for cleanup
self.active_workers.append(worker)
# Submit to thread pool
if hasattr(self.parent_page, 'thread_pool'):
self.parent_page.thread_pool.start(worker)
else:
thread_pool = QThreadPool()
self.fallback_pools.append(thread_pool)
thread_pool.start(worker)
def find_track_index(self, spotify_track):
"""Find the table row index for a given Spotify track"""
for i, playlist_track in enumerate(self.playlist.tracks):
if (playlist_track.name == spotify_track.name and
playlist_track.artists[0] == spotify_track.artists[0]):
return i
return None
def on_track_download_complete(self, download_index, track_index, download_id):
"""Handle successful track download"""
print(f"✅ Download {download_index + 1} completed: {download_id}")
# Update table row
if track_index is not None:
downloaded_item = QTableWidgetItem("✅ Downloaded")
downloaded_item.setFlags(downloaded_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
downloaded_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(track_index, 4, downloaded_item)
# Update progress
self.current_download += 1
self.download_progress.setValue(self.current_download)
# Continue with next download
self.download_next_track()
def on_track_download_failed(self, download_index, track_index, error_message):
"""Handle failed track download"""
print(f"❌ Download {download_index + 1} failed: {error_message}")
# Update table row
if track_index is not None:
failed_item = QTableWidgetItem("❌ Failed")
failed_item.setFlags(failed_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
failed_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(track_index, 4, failed_item)
# Update progress and continue
self.current_download += 1
self.download_progress.setValue(self.current_download)
# Continue with next download
self.download_next_track()
def on_all_downloads_complete(self):
"""Handle completion of all downloads"""
self.download_in_progress = False
print("🎉 All downloads completed!")
# Update button text
self.begin_search_btn.setText("Downloads Complete")
QMessageBox.information(self, "Downloads Complete",
f"Completed downloading {len(self.missing_tracks)} missing tracks!")
def create_progress_bubble(self):
"""Create a progress bubble on the main sync page"""
if hasattr(self.parent_page, 'create_download_progress_bubble'):
# Calculate current progress
total_tracks = len(self.playlist.tracks)
completed_analysis = self.analysis_progress.value() if self.analysis_complete else 0
completed_downloads = self.download_progress.value() if self.download_in_progress else 0
progress_info = {
'playlist_name': self.playlist.name,
'total_tracks': total_tracks,
'analysis_complete': self.analysis_complete,
'analysis_progress': completed_analysis,
'download_progress': completed_downloads,
'download_total': len(self.missing_tracks) if hasattr(self, 'missing_tracks') else 0,
'modal_reference': self # Keep reference to reopen modal
}
self.parent_page.create_download_progress_bubble(progress_info)
def simulate_downloads(self):
"""Simulate download process (placeholder for real implementation)"""
from PyQt6.QtCore import QTimer
@ -2364,8 +2633,12 @@ class DownloadMissingTracksModal(QDialog):
def on_close_clicked(self):
"""Handle Close button - closes modal without canceling operations"""
# Don't cancel operations, just close modal
self.reject() # Close modal
# If operations are in progress, create progress bubble
if self.download_in_progress or not self.analysis_complete:
self.create_progress_bubble()
# Close modal without canceling operations
self.reject()
def cancel_operations(self):
"""Cancel any ongoing operations"""