This commit is contained in:
Broque Thomas 2025-07-23 13:04:41 -07:00
parent 183a1a623c
commit 3920d7a1c6
4 changed files with 41240 additions and 193 deletions

View file

@ -1 +1 @@
{"access_token": "BQBenQfvlSw6GKfKdG8opa34ekCbRDYZJdf0hNgUCAJsa9ql6C-7eh6-9cpPOE-ABH-MD6UYS9knFFYs9x-hupI0MYh66PQdTjw_uLJ1lYx43eA4odsccJFTGJTlWaNR0SUC83bWVdzuh-m1glVzDVzJTv6Gap8yUHbA-EwcQjOI7fPA8pgQqyU8RoF06xBsyk9bLqG9g0VnElkk7BlMDvkWxFoIUhiP3Lt2DnC3QeMsT8LYCQvEZWSvqopuUppN", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753299305, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
{"access_token": "BQA5kusm3wT-O4gvbF1tQA8jM0VZuA8joZHW3jBVJVsu8HFwtNZ2BxH3vDuXAtp_W2-M4v71rfRU9Uy7LYMIPPoo-vPddXtmNpiPDwvO4cETXsKnIRzV3vlQyGkMP9J4UQ2E7Z-McaPPtuoKyC7C_rRgzdoAD8IGPHSYdR4ABu3thOmAP_MyEtO2P6W8WArtjXHNRFL4h1UUejx2fQbyMgz56YMiWF2VxfQMS7W1-8Fyq7Yz--cA18E8v3FD_15x", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753302850, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}

41177
logs/app.log

File diff suppressed because it is too large Load diff

View file

@ -5152,7 +5152,6 @@ class DownloadMissingTracksModal(QDialog):
def on_search_query_completed_parallel(self, results, queries, spotify_track, track_index, table_index, query_index, query, download_index):
"""Handle completion of a parallel search query"""
# Check for cancellation
if hasattr(self, 'cancel_requested') and self.cancel_requested:
print("🛑 Parallel search query completed after cancellation - ignoring results")
self.on_parallel_track_completed(download_index, False)
@ -5160,43 +5159,33 @@ class DownloadMissingTracksModal(QDialog):
print(f"✅ Parallel search query '{query}' returned {len(results)} results for download {download_index + 1}")
# Update console with result count
if hasattr(self.parent_page, 'log_area'):
self.parent_page.log_area.append(f" ✅ Found {len(results)} tracks for '{query}' (parallel #{download_index + 1})")
if results and len(results) > 0:
# Debug: Show first result details
print(f"🔍 DEBUG: Processing {len(results)} results for parallel download {download_index + 1}")
if len(results) > 0:
first_result = results[0]
print(f"🔍 DEBUG: First result type: {type(first_result)}")
print(f"🔍 DEBUG: First result attributes: {[attr for attr in dir(first_result) if not attr.startswith('_')]}")
if hasattr(first_result, 'filename'):
print(f"🔍 DEBUG: First result filename: '{first_result.filename}'")
if hasattr(first_result, 'title'):
print(f"🔍 DEBUG: First result title: '{first_result.title}'")
# Found results - process using existing validation infrastructure
valid_candidates = self.get_valid_candidates(results, spotify_track, query)
if valid_candidates:
print(f"🎯 Found {len(valid_candidates)} valid candidates for parallel download {download_index + 1}")
# Start validation and download using existing infrastructure
# --- NEW CODE: Cache the candidates and the original spotify track ---
if hasattr(self, 'parallel_search_tracking') and download_index in self.parallel_search_tracking:
self.parallel_search_tracking[download_index]['candidates'] = valid_candidates
self.parallel_search_tracking[download_index]['spotify_track'] = spotify_track
# --- END OF NEW CODE ---
best_match = valid_candidates[0]
print(f"🎯 Selected best match for parallel download: {best_match.filename}")
# Update console with selection
if hasattr(self.parent_page, 'log_area'):
self.parent_page.log_area.append(f" 🎯 Best match (parallel #{download_index + 1}): {best_match.filename}")
# Use existing validation system with parallel completion handling
self.validate_slskd_result_with_spotify_parallel(best_match, spotify_track, track_index, table_index, valid_candidates, download_index)
return
# No results or failed - handle failure
self.on_parallel_track_failed(download_index, f"No valid results for query: {query}")
self.on_parallel_track_failed(download_index, f"No valid results for query: {query}")
def validate_slskd_result_with_spotify_parallel(self, slskd_result, original_spotify_track, track_index, table_index, valid_candidates, download_index):
"""Parallel version of Spotify validation - SIMPLIFIED for speed"""
print(f"🎯 Starting parallel validation for download {download_index + 1}")
@ -5340,8 +5329,9 @@ class DownloadMissingTracksModal(QDialog):
# Remove from active list so we don't check it again
self.active_downloads.remove(download_info)
# This is the crucial part: call your retry logic
self.on_parallel_track_failed(download_index, f"Download {new_status}")
# CRITICAL FIX: Do NOT call on_parallel_track_failed here.
# The retry logic now takes responsibility for the download slot.
# It will call on_parallel_track_failed itself only if all retries are exhausted.
self.retry_parallel_download_with_fallback(download_index, f"Download {new_status}")
elif new_status == 'completed':
@ -5363,6 +5353,7 @@ class DownloadMissingTracksModal(QDialog):
self._is_status_update_running = False
def check_transfer_folder_for_parallel_download(self, download_index):
"""Check if a parallel download completed and exists in Transfer folder"""
try:
@ -5419,190 +5410,69 @@ class DownloadMissingTracksModal(QDialog):
return False
def retry_parallel_download_with_fallback(self, download_index, reason):
"""Retry a failed parallel download with different source and quality fallback"""
"""
Retries a failed download by selecting the next-best candidate from the
original cached search results.
"""
try:
print(f"🔄 Retrying parallel download {download_index + 1} - reason: {reason}")
# Get track info
if not (hasattr(self, 'parallel_search_tracking') and
print(f"🔄 Retrying parallel download {download_index + 1} from cached results - reason: {reason}")
if not (hasattr(self, 'parallel_search_tracking') and
download_index in self.parallel_search_tracking):
print(f"❌ No tracking info for retry of download {download_index + 1}")
self.on_parallel_track_failed(download_index, f"Retry failed: {reason}")
self.on_parallel_track_failed(download_index, f"Retry failed: No tracking info")
return
track_info = self.parallel_search_tracking[download_index]
candidates = track_info.get('candidates', [])
used_sources = track_info.get('used_sources', set())
# Find the next unused candidate
next_candidate = None
for candidate in candidates:
source_key = f"{getattr(candidate, 'username', getattr(candidate, 'user', 'unknown'))}_{getattr(candidate, 'filename', 'unknown')}"
if source_key not in used_sources:
next_candidate = candidate
break
if not next_candidate:
print(f"❌ No alternative sources left in cache for download {download_index + 1}")
self.on_parallel_track_failed(download_index, "No alternative sources in cache")
return
print(f"🎯 Found next candidate from cache: {next_candidate.filename}")
track_index = track_info['track_index']
table_index = track_info['table_index']
spotify_track = track_info['spotify_track']
# Initialize retry tracking if not exists
if 'retry_count' not in track_info:
track_info['retry_count'] = 0
track_info['used_sources'] = set() # Track used sources to avoid repeats
track_info['retry_count'] += 1
# Max 2 retries per track
if track_info['retry_count'] > 2:
print(f"❌ Max retries exceeded for parallel download {download_index + 1}")
self.on_parallel_track_failed(download_index, f"Max retries exceeded after {reason}")
return
print(f"🔄 Retry attempt {track_info['retry_count']}/2 for: {spotify_track.name}")
# Update UI to show retrying
# Update UI to show retrying status
retry_count = len(used_sources)
if table_index is not None and table_index < self.track_table.rowCount():
retrying_item = QTableWidgetItem(f"🔄 Retry {track_info['retry_count']}")
retrying_item = QTableWidgetItem(f"🔄 Retry {retry_count}")
retrying_item.setFlags(retrying_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
retrying_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.track_table.setItem(table_index, 4, retrying_item)
# Start retry search with fallback quality preferences
self.start_retry_search_worker(download_index, track_index, table_index, spotify_track, track_info)
# Kick off the download process with the new candidate
self.on_predownload_validation_completed_parallel(
is_valid=True,
reason="Retrying with next best cached candidate",
slskd_result=next_candidate,
original_track=spotify_track,
track_index=track_index,
table_index=table_index,
valid_candidates=candidates,
download_index=download_index,
spotify_metadata=spotify_track
)
except Exception as e:
print(f"❌ Error setting up retry for parallel download {download_index + 1}: {str(e)}")
self.on_parallel_track_failed(download_index, f"Retry setup failed: {str(e)}")
def start_retry_search_worker(self, download_index, track_index, table_index, spotify_track, track_info):
"""Start background worker to search for alternative sources with quality fallback"""
from PyQt6.QtCore import QRunnable, QObject, pyqtSignal
class RetrySearchWorkerSignals(QObject):
retry_results_ready = pyqtSignal(int, int, int, list, object) # download_index, track_index, table_index, results, track_info
retry_search_failed = pyqtSignal(int, int, int, str, object) # download_index, track_index, table_index, error, track_info
class RetrySearchWorker(QRunnable):
def __init__(self, soulseek_client, spotify_track, track_info):
super().__init__()
self.soulseek_client = soulseek_client
self.spotify_track = spotify_track
self.track_info = track_info
self.signals = RetrySearchWorkerSignals()
def clean_track_name_for_retry_search(self, track_name):
"""Clean track name for retry searches (same as main cleaning)"""
import re
cleaned_name = track_name
# Remove content in parentheses - covers feat., remix, remaster, etc.
cleaned_name = re.sub(r'\s*\([^)]*\)', '', cleaned_name)
# Remove content in square brackets
cleaned_name = re.sub(r'\s*\[[^\]]*\]', '', cleaned_name)
# Remove common standalone suffixes after dashes
suffixes_to_remove = [
r'\s*-\s*remix\s*$', r'\s*-\s*radio\s+edit\s*$', r'\s*-\s*extended\s+version\s*$',
r'\s*-\s*remaster\s*$', r'\s*-\s*remastered\s*$', r'\s*-\s*live\s*$',
r'\s*-\s*acoustic\s*$', r'\s*-\s*instrumental\s*$'
]
for suffix_pattern in suffixes_to_remove:
cleaned_name = re.sub(suffix_pattern, '', cleaned_name, flags=re.IGNORECASE)
# Clean up extra whitespace
cleaned_name = ' '.join(cleaned_name.split())
# Log retry cleaning if significant changes were made
if cleaned_name != track_name:
print(f"🧹 Retry cleaned: '{track_name}''{cleaned_name}'")
return cleaned_name
def run(self):
try:
print(f"🔍 Retry search for: {self.spotify_track.name} by {self.spotify_track.artists[0] if self.spotify_track.artists else 'Unknown'}")
# Create search queries with quality fallback preferences
raw_track_name = self.spotify_track.name
artist_name = self.spotify_track.artists[0] if self.spotify_track.artists else ""
# Clean track name for better search results
track_name = self.clean_track_name_for_retry_search(raw_track_name)
# Search strategies with quality preferences (try FLAC first, then MP3)
search_queries = []
# For retry attempts, be more flexible with formats
if self.track_info['retry_count'] == 1:
# First retry: still prefer FLAC but accept MP3
search_queries = [
f"{track_name} flac",
f"{artist_name} {track_name} flac",
f"{track_name} mp3",
f"{artist_name} {track_name} mp3",
f"{track_name}", # Any format
]
else:
# Second retry: accept any quality, focus on different sources
search_queries = [
f"{track_name} mp3",
f"{artist_name} {track_name}",
f"{track_name}",
# Try with fewer keywords if artist name is complex
f"{track_name} {artist_name.split()[0]}" if len(artist_name.split()) > 1 else f"{track_name}",
]
all_results = []
# Search with each query
for query in search_queries:
if len(all_results) >= 20: # Limit total results
break
try:
print(f" 🔍 Retry query: '{query}'")
search_result = self.soulseek_client.search(query, timeout=30)
if search_result and len(search_result) > 0:
# Handle tuple format (tracks, albums)
results_list = search_result[0] if isinstance(search_result, tuple) else search_result
if results_list:
# Filter out sources we already tried
filtered_results = []
for result in results_list[:10]: # Top 10 per query
source_key = f"{getattr(result, 'username', getattr(result, 'user', 'unknown'))}_{getattr(result, 'filename', 'unknown')}"
if source_key not in self.track_info.get('used_sources', set()):
filtered_results.append(result)
all_results.extend(filtered_results)
print(f" ✅ Found {len(filtered_results)} new sources from '{query}'")
except Exception as search_error:
print(f" ❌ Retry search query failed '{query}': {search_error}")
continue
if all_results:
print(f"🎯 Retry search found {len(all_results)} alternative sources")
self.signals.retry_results_ready.emit(download_index, track_index, table_index, all_results, self.track_info)
else:
print(f"❌ Retry search found no alternative sources")
self.signals.retry_search_failed.emit(download_index, track_index, table_index, "No alternative sources found", self.track_info)
except Exception as e:
print(f"❌ Retry search worker error: {str(e)}")
self.signals.retry_search_failed.emit(download_index, track_index, table_index, str(e), self.track_info)
# Create and start worker
worker = RetrySearchWorker(self.parent_page.soulseek_client, spotify_track, track_info)
worker.signals.retry_results_ready.connect(self.handle_retry_search_results)
worker.signals.retry_search_failed.connect(self.handle_retry_search_failed)
# Store worker reference
self.active_workers.append(worker)
# Submit to thread pool
if hasattr(self.parent_page, 'thread_pool'):
self.parent_page.thread_pool.start(worker)
else:
from PyQt6.QtCore import QThreadPool
thread_pool = QThreadPool()
self.fallback_pools.append(thread_pool)
thread_pool.start(worker)
print(f"❌ Error during retry from cache for download {download_index + 1}: {str(e)}")
import traceback
traceback.print_exc()
self.on_parallel_track_failed(download_index, f"Retry from cache failed: {str(e)}")
def handle_retry_search_results(self, download_index, track_index, table_index, results, track_info):
"""Handle retry search results and start best alternative download"""
try: