This commit is contained in:
Broque Thomas 2025-07-22 19:57:05 -07:00
parent ccc89067e6
commit 707b09028a
4 changed files with 3238 additions and 88 deletions

File diff suppressed because it is too large Load diff

View file

@ -1600,6 +1600,23 @@ class SyncPage(QWidget):
# Log area # Log area
self.log_area = QTextEdit() self.log_area = QTextEdit()
self.log_area.setMinimumHeight(80) # Set minimum height instead of maximum self.log_area.setMinimumHeight(80) # Set minimum height instead of maximum
# Override append method to limit to 200 lines
original_append = self.log_area.append
def limited_append(text):
original_append(text)
# Keep only last 200 lines
text_content = self.log_area.toPlainText()
lines = text_content.split('\n')
if len(lines) > 200:
trimmed_lines = lines[-200:]
self.log_area.setPlainText('\n'.join(trimmed_lines))
# Move cursor to end
cursor = self.log_area.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
self.log_area.setTextCursor(cursor)
self.log_area.append = limited_append
self.log_area.setStyleSheet(""" self.log_area.setStyleSheet("""
QTextEdit { QTextEdit {
background: #181818; background: #181818;
@ -1856,9 +1873,11 @@ class DownloadMissingTracksModal(QDialog):
self.total_tracks = len(playlist.tracks) self.total_tracks = len(playlist.tracks)
self.matched_tracks_count = 0 self.matched_tracks_count = 0
self.tracks_to_download_count = 0 self.tracks_to_download_count = 0
self.downloaded_tracks_count = 0
self.analysis_complete = False self.analysis_complete = False
self.download_in_progress = False self.download_in_progress = False
print(f"📊 Total tracks: {self.total_tracks}") print(f"📊 Total tracks: {self.total_tracks}")
# Track analysis results # Track analysis results
@ -1976,9 +1995,13 @@ class DownloadMissingTracksModal(QDialog):
# To Download # To Download
self.download_card = self.create_compact_counter_card("⬇️ Missing", "0", "#ff6b6b") self.download_card = self.create_compact_counter_card("⬇️ Missing", "0", "#ff6b6b")
# Downloaded
self.downloaded_card = self.create_compact_counter_card("✅ Downloaded", "0", "#4CAF50")
dashboard_layout.addWidget(self.total_card) dashboard_layout.addWidget(self.total_card)
dashboard_layout.addWidget(self.matched_card) dashboard_layout.addWidget(self.matched_card)
dashboard_layout.addWidget(self.download_card) dashboard_layout.addWidget(self.download_card)
dashboard_layout.addWidget(self.downloaded_card)
dashboard_layout.addStretch() dashboard_layout.addStretch()
header_layout.addLayout(title_section) header_layout.addLayout(title_section)
@ -2058,6 +2081,8 @@ class DownloadMissingTracksModal(QDialog):
self.matched_count_label = count_label self.matched_count_label = count_label
elif "Missing" in title: elif "Missing" in title:
self.download_count_label = count_label self.download_count_label = count_label
elif "Downloaded" in title:
self.downloaded_count_label = count_label
return card return card
@ -4003,52 +4028,111 @@ class DownloadMissingTracksModal(QDialog):
self.track_download_items = getattr(self, 'track_download_items', {}) self.track_download_items = getattr(self, 'track_download_items', {})
self.track_download_items[download_item] = (track_index, table_index) self.track_download_items[download_item] = (track_index, table_index)
# Monitor download completion # Monitor download completion - but don't validate until actual completion
self.monitor_download_completion(download_item, track_index, table_index) self.monitor_download_completion(download_item, track_index, table_index)
# Return download item for tracking
return download_item
else: else:
# Download failed to start # Download failed to start
self.on_track_download_failed_infrastructure(track_index, table_index, "Failed to start download") self.on_track_download_failed_infrastructure(track_index, table_index, "Failed to start download")
return None
except Exception as e: except Exception as e:
print(f"❌ Error starting download via infrastructure: {str(e)}") print(f"❌ Error starting download via infrastructure: {str(e)}")
self.on_track_download_failed_infrastructure(track_index, table_index, str(e)) self.on_track_download_failed_infrastructure(track_index, table_index, str(e))
return None
def monitor_download_completion(self, download_item, track_index, table_index): def monitor_download_completion(self, download_item, track_index, table_index):
"""Monitor download completion using QTimer""" """Monitor download completion by checking Transfer folder for completed files"""
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
import os
# Create timer to check download status # Get the expected transfer path for this track
timer = QTimer() if track_index < len(self.missing_tracks):
timer.timeout.connect(lambda: self.check_download_status(download_item, track_index, table_index, timer)) spotify_track = self.missing_tracks[track_index].spotify_track
timer.start(1000) # Check every second expected_transfer_path = self.get_expected_transfer_path(spotify_track)
# Store timer reference for cleanup # Create timer to check for file existence in Transfer folder
if not hasattr(self, 'download_timers'): timer = QTimer()
self.download_timers = [] timer.expected_path = expected_transfer_path
self.download_timers.append(timer) timer.track_index = track_index
timer.table_index = table_index
timer.start_time = 0
timer.timeout.connect(lambda: self.check_transfer_folder_completion(timer))
timer.start(2000) # Check every 2 seconds (less frequent than queue checking)
# Store timer reference for cleanup
if not hasattr(self, 'download_timers'):
self.download_timers = []
self.download_timers.append(timer)
else:
print(f"❌ Track index {track_index} out of range for monitoring")
def check_download_status(self, download_item, track_index, table_index, timer): def get_expected_transfer_path(self, spotify_track):
"""Check if download is complete""" """Get the expected path where this track should appear in Transfer folder"""
import os
# Use the first artist for folder structure
artist_name = spotify_track.artists[0] if spotify_track.artists else "Unknown Artist"
track_name = spotify_track.name
# Clean names for file system
clean_artist = self.sanitize_filename(artist_name)
clean_track = self.sanitize_filename(track_name)
# Expected pattern: Transfer/ARTIST_NAME/ARTIST_NAME - TRACK_NAME/
# The actual filename will vary, so we'll check for the folder existence
expected_folder = os.path.join("Transfer", clean_artist, f"{clean_artist} - {clean_track}")
return expected_folder
def sanitize_filename(self, filename):
"""Clean filename for cross-platform compatibility"""
import re
# Remove or replace invalid characters
filename = re.sub(r'[<>:"/\\|?*]', '', filename)
filename = filename.strip()
return filename
def check_transfer_folder_completion(self, timer):
"""Check if the expected file/folder exists in Transfer directory"""
import os
import glob
try: try:
# Check if download item still exists and get its status # Increment timer counter
if hasattr(download_item, 'download_id') and download_item.download_id: timer.start_time += 2 # 2 seconds per check
# Use async function to get download status properly
self.check_download_status_async(download_item, track_index, table_index, timer)
return
# Check if timer has been running too long (timeout after 5 minutes) # Check if expected folder exists
if not hasattr(timer, 'start_time'): if os.path.exists(timer.expected_path):
timer.start_time = 0 # Check if there are any audio files in the folder
timer.start_time += 1 audio_files = glob.glob(os.path.join(timer.expected_path, "*.flac")) + \
glob.glob(os.path.join(timer.expected_path, "*.mp3")) + \
glob.glob(os.path.join(timer.expected_path, "*.wav"))
if audio_files:
print(f"✅ Transfer folder completion detected: {timer.expected_path}")
timer.stop()
self.on_track_download_complete_infrastructure(timer.track_index, timer.table_index)
return
if timer.start_time > 300: # 5 minutes timeout # Timeout after 10 minutes
if timer.start_time > 600: # 10 minutes
print(f"⏰ Transfer folder check timeout for: {timer.expected_path}")
timer.stop() timer.stop()
self.on_track_download_failed_infrastructure(track_index, table_index, "Download timeout") self.on_track_download_failed_infrastructure(timer.track_index, timer.table_index, "Transfer timeout")
except Exception as e: except Exception as e:
print(f"❌ Error checking download status: {str(e)}") print(f"❌ Error checking transfer folder: {str(e)}")
timer.stop() timer.stop()
self.on_track_download_failed_infrastructure(track_index, table_index, str(e)) self.on_track_download_failed_infrastructure(timer.track_index, timer.table_index, str(e))
def check_download_status(self, download_item, track_index, table_index, timer):
"""DEPRECATED: Old download status checking - now using Transfer folder detection"""
# This method is kept for compatibility but not used anymore
print("⚠️ Using deprecated download status checking - should use Transfer folder detection")
timer.stop()
def check_download_status_async(self, download_item, track_index, table_index, timer): def check_download_status_async(self, download_item, track_index, table_index, timer):
"""Check download status using proper async handling""" """Check download status using proper async handling"""
@ -4138,22 +4222,39 @@ class DownloadMissingTracksModal(QDialog):
self.on_track_download_failed_infrastructure(track_index, table_index, f"Status check failed: {error}") self.on_track_download_failed_infrastructure(track_index, table_index, f"Status check failed: {error}")
def on_track_download_complete_infrastructure(self, track_index, table_index): def on_track_download_complete_infrastructure(self, track_index, table_index):
"""Handle successful track download via infrastructure - with Spotify validation""" """Handle successful track download via infrastructure - ALREADY VALIDATED PRE-DOWNLOAD"""
print(f"🔍 Download {track_index + 1} completed, starting Spotify validation...") print(f"✅ Download {track_index + 1} completed via infrastructure (pre-validated)")
# Get the original track for validation # These downloads were already validated before starting, so mark as completed immediately
if track_index < len(self.missing_tracks): if table_index is not None:
original_track = self.missing_tracks[track_index].spotify_track downloaded_item = QTableWidgetItem("✅ Downloaded")
self.validate_downloaded_track(track_index, table_index, original_track) downloaded_item.setFlags(downloaded_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
else: downloaded_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
print(f"❌ Track index {track_index} out of range, marking as failed") self.track_table.setItem(table_index, 4, downloaded_item)
self.on_track_download_failed_infrastructure(track_index, table_index, "Track index out of range")
# Update console log
if hasattr(self.parent_page, 'log_area') and track_index < len(self.missing_tracks):
track = self.missing_tracks[track_index].spotify_track
track_name = track.name
artist_name = track.artists[0] if track.artists else "Unknown"
self.parent_page.log_area.append(f"✅ Downloaded & validated: {track_name} by {artist_name}")
# Update counters (these tracks were pre-validated, so count as successful)
self.downloaded_tracks_count += 1
if hasattr(self, 'downloaded_count_label'):
self.downloaded_count_label.setText(str(self.downloaded_tracks_count))
self.successful_downloads += 1
self.completed_downloads += 1
# Update progress and continue
self.advance_to_next_track()
def validate_downloaded_track(self, track_index, table_index, original_track): def validate_downloaded_track(self, track_index, table_index, original_track):
"""Validate that downloaded track matches original Spotify track via API lookup""" """Validate that downloaded track matches original Spotify track via API lookup"""
print(f"🎯 Validating: {original_track.name} by {original_track.artists[0] if original_track.artists else 'Unknown'}") print(f"🎯 Starting validation: {original_track.name} by {original_track.artists[0] if original_track.artists else 'Unknown'}")
# Update table to show validation in progress # Update table to show validation in progress - ONLY when download is actually complete
if table_index is not None: if table_index is not None:
validating_item = QTableWidgetItem("🔍 Validating") validating_item = QTableWidgetItem("🔍 Validating")
validating_item.setFlags(validating_item.flags() & ~Qt.ItemFlag.ItemIsEditable) validating_item.setFlags(validating_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
@ -4417,7 +4518,7 @@ class DownloadMissingTracksModal(QDialog):
# This ensures the download uses validated Spotify metadata for folder structure # This ensures the download uses validated Spotify metadata for folder structure
spotify_based_result = self.create_spotify_based_search_result_from_validation(slskd_result, spotify_metadata) spotify_based_result = self.create_spotify_based_search_result_from_validation(slskd_result, spotify_metadata)
# Update table to show downloading status # Update table to show downloading status (don't mark as downloaded yet)
if table_index is not None and table_index < self.track_table.rowCount(): if table_index is not None and table_index < self.track_table.rowCount():
downloading_item = QTableWidgetItem("⏬ Downloading") downloading_item = QTableWidgetItem("⏬ Downloading")
downloading_item.setFlags(downloading_item.flags() & ~Qt.ItemFlag.ItemIsEditable) downloading_item.setFlags(downloading_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
@ -4429,7 +4530,10 @@ class DownloadMissingTracksModal(QDialog):
self.parent_page.log_area.append(f"🎵 Downloading: {spotify_based_result.filename} (validated)") self.parent_page.log_area.append(f"🎵 Downloading: {spotify_based_result.filename} (validated)")
# Use downloads.py infrastructure for the actual download with validated metadata # Use downloads.py infrastructure for the actual download with validated metadata
self.start_matched_download_via_infrastructure(spotify_based_result, track_index, table_index) download_id = self.start_matched_download_via_infrastructure(spotify_based_result, track_index, table_index)
# The download is already being monitored by the existing monitor_download_completion method
# No additional tracking needed here
# Move to next track search # Move to next track search
self.advance_to_next_track() self.advance_to_next_track()
@ -4469,6 +4573,7 @@ class DownloadMissingTracksModal(QDialog):
self.album = spotify_album_name self.album = spotify_album_name
return SpotifyBasedSearchResult() return SpotifyBasedSearchResult()
def start_spotify_validation_worker(self, track_index, table_index, original_track): def start_spotify_validation_worker(self, track_index, table_index, original_track):
"""Start background worker to validate track via Spotify API""" """Start background worker to validate track via Spotify API"""
@ -4566,6 +4671,11 @@ class DownloadMissingTracksModal(QDialog):
downloaded_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) downloaded_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(table_index, 4, downloaded_item) self.track_table.setItem(table_index, 4, downloaded_item)
# Update downloaded counter in dashboard
self.downloaded_tracks_count += 1
if hasattr(self, 'downloaded_count_label'):
self.downloaded_count_label.setText(str(self.downloaded_tracks_count))
# Update progress # Update progress
self.completed_downloads += 1 self.completed_downloads += 1
self.advance_to_next_track() self.advance_to_next_track()
@ -4688,21 +4798,30 @@ class DownloadMissingTracksModal(QDialog):
downloading_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) downloading_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(track_index, 4, downloading_item) self.track_table.setItem(track_index, 4, downloading_item)
# Create download worker # NEW: Use infrastructure path for proper Transfer folder organization
worker = TrackDownloadWorker(track, self.parent_page.soulseek_client, self.current_download, track_index) # Instead of the simple TrackDownloadWorker, use the search-and-infrastructure path
worker.signals.download_started.connect(self.on_track_download_started) # This ensures downloads go to Transfer folder with Spotify metadata organization
worker.signals.download_failed.connect(self.on_track_download_failed) self.search_and_download_track_with_infrastructure(track, self.current_download, track_index)
# Track worker for cleanup # Increment and continue (don't wait for completion)
self.active_workers.append(worker) self.current_download += 1
# Submit to thread pool # Continue with next download
if hasattr(self.parent_page, 'thread_pool'): self.download_next_track()
self.parent_page.thread_pool.start(worker)
else: def search_and_download_track_with_infrastructure(self, spotify_track, download_index, track_index):
thread_pool = QThreadPool() """Search for track and download via infrastructure path (with Transfer folder organization)"""
self.fallback_pools.append(thread_pool) print(f"🔍 Starting search + infrastructure download for: {spotify_track.name}")
thread_pool.start(worker)
# Create search queries using the smart strategy
track_name = spotify_track.name
artist_name = spotify_track.artists[0] if spotify_track.artists else ""
search_queries = self.generate_smart_search_queries(track_name, artist_name)
# Start the search process using the existing infrastructure
# This will trigger search → validation → download → Transfer folder organization
self.start_track_search_with_queries(spotify_track, search_queries, track_index, track_index)
def find_track_index(self, spotify_track): def find_track_index(self, spotify_track):
"""Find the table row index for a given Spotify track""" """Find the table row index for a given Spotify track"""
@ -4914,44 +5033,19 @@ class DownloadMissingTracksModal(QDialog):
self.on_actual_track_download_complete(download_index, track_index) self.on_actual_track_download_complete(download_index, track_index)
def on_actual_track_download_complete(self, download_index, track_index): def on_actual_track_download_complete(self, download_index, track_index):
"""Handle when a download is actually completed (not just started)""" """Handle when a download is actually completed - NOW ONLY USED FOR PRE-VALIDATION SYSTEM"""
print(f"✅ Download {download_index + 1} actually completed!") print(f"🔍 Download {download_index + 1} completed, will validate via Spotify validation system")
# Update table to show Downloaded # Don't mark as downloaded here - let the validation system handle it
if track_index is not None: # Just update internal tracking that the download file is ready for validation
downloaded_item = QTableWidgetItem("✅ Downloaded") if track_index is not None and track_index < self.track_table.rowCount():
downloaded_item.setFlags(downloaded_item.flags() & ~Qt.ItemFlag.ItemIsEditable) validating_item = QTableWidgetItem("🔍 Validating")
downloaded_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) validating_item.setFlags(validating_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.track_table.setItem(track_index, 4, downloaded_item) validating_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(track_index, 4, validating_item)
# Update completion tracking # The actual completion tracking is now handled by on_validation_completed()
self.successful_downloads += 1 # which properly waits for Spotify validation before marking as downloaded
self.completed_downloads += 1
# Update main console log
if hasattr(self.parent_page, 'log_area') and download_index < len(self.missing_tracks):
track = self.missing_tracks[download_index].spotify_track
track_name = track.name
artist_name = track.artists[0] if track.artists else "Unknown Artist"
remaining = len(self.missing_tracks) - self.successful_downloads
self.parent_page.log_area.append(f"✅ Downloaded: {track_name} by {artist_name} ({remaining} remaining)")
# Update progress bar to reflect actual completions
self.download_progress.setValue(self.completed_downloads)
# Update playlist status indicator
playlist_item = self.find_playlist_item()
if playlist_item:
total = len(self.missing_tracks) if hasattr(self, 'missing_tracks') else 0
if self.completed_downloads >= total:
self.on_all_downloads_complete()
else:
status_text = f"⏬ Downloading {self.completed_downloads}/{total}"
playlist_item.update_operation_status(status_text)
# Check if all downloads are complete
if self.completed_downloads >= len(self.missing_tracks):
self.on_all_downloads_complete()
def setup_background_status_updates(self): def setup_background_status_updates(self):
"""Set up timer-based background status updates for playlist indicator""" """Set up timer-based background status updates for playlist indicator"""