This commit is contained in:
Broque Thomas 2025-07-23 11:42:49 -07:00
parent 299e968522
commit 183a1a623c
4 changed files with 8648 additions and 133 deletions

View file

@ -1 +1 @@
{"access_token": "BQCd4iKWQV4PxyAyDjSQ3PB5lLp56bxbfMoBL1m_8-d9lxTyUbKj7KHiNpS7zT_YWwjn7TBB2UrEUu9R9RKsYQ8B1LfHMWZzgqzGAlMEJ7vhV0zEFYaBryIRa92-p2Jxp3uBDy_IfNKJKo8Xxa6yirR3LabVrylitbjq27TddFVlE1pVZA69iM58NKwLwT578vQ6D8hgv96izhIcbmHc5lcH84y3OnXAcOgnXJSUyiEUluCYqbXrubweoV0HYUSU", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753252998, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"} {"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"}

File diff suppressed because it is too large Load diff

View file

@ -223,6 +223,100 @@ class TrackDownloadWorker(QRunnable):
except Exception as e: except Exception as e:
self.signals.download_failed.emit(self.download_index, self.track_index, str(e)) self.signals.download_failed.emit(self.download_index, self.track_index, str(e))
class SyncStatusProcessingWorkerSignals(QObject):
"""Defines the signals available from the SyncStatusProcessingWorker."""
completed = pyqtSignal(list)
error = pyqtSignal(str)
class SyncStatusProcessingWorker(QRunnable):
"""
Runs download status processing in a background thread for the sync modal.
It checks the slskd API and returns a list of updates for the main thread.
"""
def __init__(self, soulseek_client, download_items_data):
super().__init__()
self.signals = SyncStatusProcessingWorkerSignals()
self.soulseek_client = soulseek_client
self.download_items_data = download_items_data
def run(self):
"""The main logic of the background worker."""
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
transfers_data = loop.run_until_complete(
self.soulseek_client._make_request('GET', 'transfers/downloads')
)
loop.close()
results = []
if not transfers_data:
self.signals.completed.emit([])
return
all_transfers = [
file for user_data in transfers_data if 'directories' in user_data
for directory in user_data['directories'] if 'files' in directory
for file in directory['files']
]
transfers_by_id = {t['id']: t for t in all_transfers}
for item_data in self.download_items_data:
matching_transfer = transfers_by_id.get(item_data['download_id'])
if not matching_transfer:
import os
dl_filename = os.path.basename(item_data['file_path']).lower()
for t in all_transfers:
if os.path.basename(t.get('filename', '')).lower() == dl_filename:
matching_transfer = t
break
if matching_transfer:
state = matching_transfer.get('state', 'Unknown')
progress = matching_transfer.get('percentComplete', 0)
new_status = 'queued'
# Correct order of checks: Terminal states first.
if 'Cancelled' in state or 'Canceled' in state:
new_status = 'cancelled'
elif 'Failed' in state or 'Errored' in state:
new_status = 'failed'
elif 'Completed' in state or 'Succeeded' in state:
new_status = 'completed'
elif 'InProgress' in state:
new_status = 'downloading'
else:
new_status = 'queued'
payload = {
'widget_id': item_data['widget_id'], # This will be the download_index
'status': new_status,
'progress': int(progress),
'transfer_id': matching_transfer.get('id'),
'username': matching_transfer.get('username')
}
results.append(payload)
else:
# Handle downloads that disappear from the API
item_data['api_missing_count'] = item_data.get('api_missing_count', 0) + 1
if item_data['api_missing_count'] >= 3:
payload = {
'widget_id': item_data['widget_id'],
'status': 'failed',
'transfer_id': item_data['download_id'],
}
results.append(payload)
self.signals.completed.emit(results)
except Exception as e:
import traceback
traceback.print_exc()
self.signals.error.emit(str(e))
class PlaylistLoaderThread(QThread): class PlaylistLoaderThread(QThread):
playlist_loaded = pyqtSignal(object) # Single playlist playlist_loaded = pyqtSignal(object) # Single playlist
loading_finished = pyqtSignal(int) # Total count loading_finished = pyqtSignal(int) # Total count
@ -1863,7 +1957,7 @@ class DownloadMissingTracksModal(QDialog):
def __init__(self, playlist, parent_page, sync_modal, downloads_page): def __init__(self, playlist, parent_page, sync_modal, downloads_page):
print(f"🏗️ Initializing DownloadMissingTracksModal...") print(f"🏗️ Initializing DownloadMissingTracksModal...")
super().__init__(sync_modal) # Set sync modal as parent super().__init__(sync_modal)
self.playlist = playlist self.playlist = playlist
self.parent_page = parent_page self.parent_page = parent_page
self.sync_modal = sync_modal self.sync_modal = sync_modal
@ -1877,7 +1971,6 @@ class DownloadMissingTracksModal(QDialog):
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
@ -1888,6 +1981,18 @@ class DownloadMissingTracksModal(QDialog):
self.active_workers = [] self.active_workers = []
self.fallback_pools = [] self.fallback_pools = []
# --- NEW CODE FOR STATUS POLLING ---
self.download_status_pool = QThreadPool()
self.download_status_pool.setMaxThreadCount(1)
self._is_status_update_running = False
self.download_status_timer = QTimer(self)
self.download_status_timer.timeout.connect(self.poll_all_download_statuses)
self.download_status_timer.start(2000) # Check statuses every 2 seconds
self.active_downloads = [] # This will hold info about tracks being downloaded
# --- END OF NEW CODE ---
print("🎨 Setting up UI...") print("🎨 Setting up UI...")
self.setup_ui() self.setup_ui()
print("✅ Modal initialization complete") print("✅ Modal initialization complete")
@ -4112,6 +4217,7 @@ class DownloadMissingTracksModal(QDialog):
else: else:
print(f"❌ Track index {track_index} out of range for monitoring") print(f"❌ Track index {track_index} out of range for monitoring")
def get_expected_transfer_path(self, spotify_track): def get_expected_transfer_path(self, spotify_track):
"""Get the expected path where this track should appear in Transfer folder""" """Get the expected path where this track should appear in Transfer folder"""
import os import os
@ -5149,20 +5255,26 @@ class DownloadMissingTracksModal(QDialog):
def start_matched_download_via_infrastructure_parallel(self, spotify_based_result, track_index, table_index, download_index): def start_matched_download_via_infrastructure_parallel(self, spotify_based_result, track_index, table_index, download_index):
"""Start infrastructure download with parallel completion tracking""" """Start infrastructure download with parallel completion tracking"""
try: try:
# Create artist object for infrastructure
artist = type('Artist', (), { artist = type('Artist', (), {
'name': spotify_based_result.artist, 'name': spotify_based_result.artist,
'image_url': None 'image_url': None
})() })()
# Call downloads.py infrastructure with validated result
download_item = self.downloads_page._start_download_with_artist(spotify_based_result, artist) download_item = self.downloads_page._start_download_with_artist(spotify_based_result, artist)
if download_item: if download_item:
print(f"✅ Successfully queued parallel download {download_index + 1}") print(f"✅ Successfully queued parallel download {download_index + 1}")
# Monitor completion with parallel handling # --- THIS IS THE NEW PART ---
self.monitor_download_completion_parallel(download_item, track_index, table_index, download_index) # Add this download to our active list for status monitoring
self.active_downloads.append({
'download_index': download_index,
'track_index': track_index,
'table_index': table_index,
'download_id': download_item.download_id,
'slskd_result': spotify_based_result, # The result object for matching
})
# --- END OF NEW PART ---
return download_item return download_item
else: else:
@ -5174,134 +5286,82 @@ class DownloadMissingTracksModal(QDialog):
self.on_parallel_track_failed(download_index, str(e)) self.on_parallel_track_failed(download_index, str(e))
return None return None
def monitor_download_completion_parallel(self, download_item, track_index, table_index, download_index): def poll_all_download_statuses(self):
"""Monitor parallel download completion using the same system as regular downloads""" """
from PyQt6.QtCore import QTimer Starts the background worker to process download statuses without blocking the UI.
This is called by the new QTimer.
"""
if self._is_status_update_running or not self.active_downloads:
return
# Store download tracking info self._is_status_update_running = True
if not hasattr(self, 'parallel_download_tracking'):
self.parallel_download_tracking = {}
self.parallel_download_tracking[download_index] = { items_to_check = []
'download_id': download_item.download_id, for download_info in self.active_downloads[:]: # Iterate over a copy
'track_index': track_index, items_to_check.append({
'table_index': table_index, 'widget_id': download_info['download_index'],
'download_index': download_index, 'download_id': download_info['download_id'],
'completed': False, 'file_path': download_info['slskd_result'].filename,
'last_seen_state': None, 'status': 'downloading',
'stuck_state_count': 0 # Track how long it's been in same state })
}
# Create timer to check download status via slskd API (same as regular downloads) if not items_to_check:
timer = QTimer() self._is_status_update_running = False
timer.download_id = download_item.download_id return
timer.track_index = track_index
timer.table_index = table_index
timer.download_index = download_index
timer.start_time = 0
timer.timeout.connect(lambda: self.check_download_status_parallel(timer))
timer.start(1000) # Check every 1 second for faster error detection
# Store timer reference for cleanup worker = SyncStatusProcessingWorker(
if not hasattr(self, 'download_timers'): soulseek_client=self.parent_page.soulseek_client,
self.download_timers = [] download_items_data=items_to_check
self.download_timers.append(timer) )
worker.signals.completed.connect(self._handle_processed_status_updates)
worker.signals.error.connect(lambda e: print(f"Status Worker Error: {e}"))
self.download_status_pool.start(worker)
def check_download_status_parallel(self, timer): def _handle_processed_status_updates(self, results):
"""Check parallel download status using hybrid API + Transfer folder approach""" """
try: This runs on the main thread and applies status updates from the background worker.
# Increment timer counter This is where the retry logic is triggered.
timer.start_time += 1 # 1 second per check """
for result in results:
download_index = result['widget_id']
new_status = result['status']
# Check if this download was already completed download_info = next((d for d in self.active_downloads if d['download_index'] == download_index), None)
if (hasattr(self, 'parallel_download_tracking') and if not download_info:
timer.download_index in self.parallel_download_tracking and continue
self.parallel_download_tracking[timer.download_index]['completed']):
timer.stop()
return
download_found_in_api = False if result.get('transfer_id'):
download_info['download_id'] = result['transfer_id']
# Step 1: Try to find download in slskd API if new_status in ['failed', 'cancelled']:
if hasattr(self.parent_page, 'soulseek_client') and self.parent_page.soulseek_client: print(f"Track {download_index} failed/cancelled. Triggering retry...")
try: self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("🔄 Retrying..."))
all_downloads = self.parent_page.soulseek_client.get_all_downloads()
# Find our download by ID # Remove from active list so we don't check it again
for download in all_downloads: self.active_downloads.remove(download_info)
if hasattr(download, 'id') and download.id == timer.download_id:
download_found_in_api = True
state = getattr(download, 'state', 'Unknown')
# Check for stuck downloads by tracking state changes # This is the crucial part: call your retry logic
tracking_info = self.parallel_download_tracking.get(timer.download_index, {}) self.on_parallel_track_failed(download_index, f"Download {new_status}")
last_state = tracking_info.get('last_seen_state') self.retry_parallel_download_with_fallback(download_index, f"Download {new_status}")
if last_state == state: elif new_status == 'completed':
# Same state as last check - increment stuck counter print(f"Track {download_index} completed successfully.")
tracking_info['stuck_state_count'] = tracking_info.get('stuck_state_count', 0) + 1 self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("✅ Downloaded"))
else:
# State changed - reset stuck counter
tracking_info['stuck_state_count'] = 0
tracking_info['last_seen_state'] = state
print(f"🔍 Parallel download {timer.download_index + 1} state: {state}")
if state == "Completed": # Remove from active downloads list
print(f"✅ Parallel download {timer.download_index + 1} completed via API") self.active_downloads.remove(download_info)
timer.stop()
self.on_parallel_track_completed(timer.download_index, True)
return
elif state in ["Cancelled", "Failed"]:
print(f"❌ Parallel download {timer.download_index + 1} failed via API: {state}")
timer.stop()
# Try to retry with different source instead of marking as failed
self.retry_parallel_download_with_fallback(timer.download_index, f"Download {state.lower()}")
return
elif state in ["InProgress", "Queued"] and tracking_info.get('stuck_state_count', 0) > 30:
# Been in same state for 30+ seconds - likely stuck
print(f"⚠️ Download {timer.download_index + 1} stuck in {state} for {tracking_info['stuck_state_count']}s - initiating retry")
timer.stop()
self.retry_parallel_download_with_fallback(timer.download_index, f"Stuck in {state}")
return
# For other states, continue monitoring
break
except Exception as api_error: # Signal completion to the parallel download manager
print(f"⚠️ API error checking parallel download {timer.download_index + 1}: {api_error}") self.on_parallel_track_completed(download_index, success=True)
# Continue to Transfer folder check
# Step 2: If not found in API, check Transfer folder (likely completed & cleaned up) elif new_status == 'downloading':
if not download_found_in_api: progress = result.get('progress', 0)
print(f"🔍 Parallel download {timer.download_index + 1} not in API - checking Transfer folder...") self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem(f"⏬ Downloading ({progress}%)"))
if self.check_transfer_folder_for_parallel_download(timer.download_index): elif new_status == 'queued':
print(f"✅ Parallel download {timer.download_index + 1} found in Transfer folder") self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("... Queued"))
timer.stop()
self.on_parallel_track_completed(timer.download_index, True)
return
else:
# Not in API and not in Transfer folder - this might indicate a problem
if timer.start_time > 30: # After 30 seconds, start checking more aggressively
print(f"⏳ Parallel download {timer.download_index + 1} not in API or Transfer folder (monitoring {timer.start_time}s)")
# After 90 seconds with no progress, assume it's stuck/failed self._is_status_update_running = False
if timer.start_time > 90: # 90 seconds - much faster than 10 minutes
print(f"⚠️ Download {timer.download_index + 1} appears stuck after {timer.start_time}s - initiating retry")
timer.stop()
self.retry_parallel_download_with_fallback(timer.download_index, "Download appears stuck/failed")
return
# Final timeout after 3 minutes (much more aggressive)
if timer.start_time > 180: # 3 minutes - downloads should not take this long
print(f"⏰ Parallel download {timer.download_index + 1} final timeout after {timer.start_time}s")
timer.stop()
# Try to retry with different source instead of marking as failed
self.retry_parallel_download_with_fallback(timer.download_index, "Final timeout")
except Exception as e:
print(f"❌ Error checking parallel download status: {str(e)}")
timer.stop()
self.on_parallel_track_failed(timer.download_index, str(e))
def check_transfer_folder_for_parallel_download(self, download_index): def check_transfer_folder_for_parallel_download(self, download_index):
"""Check if a parallel download completed and exists in Transfer folder""" """Check if a parallel download completed and exists in Transfer folder"""