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:
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):
playlist_loaded = pyqtSignal(object) # Single playlist
loading_finished = pyqtSignal(int) # Total count
@ -1863,7 +1957,7 @@ class DownloadMissingTracksModal(QDialog):
def __init__(self, playlist, parent_page, sync_modal, downloads_page):
print(f"🏗️ Initializing DownloadMissingTracksModal...")
super().__init__(sync_modal) # Set sync modal as parent
super().__init__(sync_modal)
self.playlist = playlist
self.parent_page = parent_page
self.sync_modal = sync_modal
@ -1877,7 +1971,6 @@ class DownloadMissingTracksModal(QDialog):
self.analysis_complete = False
self.download_in_progress = False
print(f"📊 Total tracks: {self.total_tracks}")
# Track analysis results
@ -1887,11 +1980,23 @@ class DownloadMissingTracksModal(QDialog):
# Worker tracking
self.active_workers = []
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...")
self.setup_ui()
print("✅ Modal initialization complete")
def setup_ui(self):
"""Set up the enhanced modal UI"""
self.setWindowTitle(f"Download Missing Tracks - {self.playlist.name}")
@ -4112,6 +4217,7 @@ class DownloadMissingTracksModal(QDialog):
else:
print(f"❌ Track index {track_index} out of range for monitoring")
def get_expected_transfer_path(self, spotify_track):
"""Get the expected path where this track should appear in Transfer folder"""
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):
"""Start infrastructure download with parallel completion tracking"""
try:
# Create artist object for infrastructure
artist = type('Artist', (), {
'name': spotify_based_result.artist,
'image_url': None
})()
# Call downloads.py infrastructure with validated result
download_item = self.downloads_page._start_download_with_artist(spotify_based_result, artist)
if download_item:
print(f"✅ Successfully queued parallel download {download_index + 1}")
# Monitor completion with parallel handling
self.monitor_download_completion_parallel(download_item, track_index, table_index, download_index)
# --- THIS IS THE NEW PART ---
# 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
else:
@ -5174,135 +5286,83 @@ class DownloadMissingTracksModal(QDialog):
self.on_parallel_track_failed(download_index, str(e))
return None
def monitor_download_completion_parallel(self, download_item, track_index, table_index, download_index):
"""Monitor parallel download completion using the same system as regular downloads"""
from PyQt6.QtCore import QTimer
def poll_all_download_statuses(self):
"""
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
self._is_status_update_running = True
# Store download tracking info
if not hasattr(self, 'parallel_download_tracking'):
self.parallel_download_tracking = {}
self.parallel_download_tracking[download_index] = {
'download_id': download_item.download_id,
'track_index': track_index,
'table_index': table_index,
'download_index': download_index,
'completed': False,
'last_seen_state': None,
'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)
timer = QTimer()
timer.download_id = download_item.download_id
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
if not hasattr(self, 'download_timers'):
self.download_timers = []
self.download_timers.append(timer)
def check_download_status_parallel(self, timer):
"""Check parallel download status using hybrid API + Transfer folder approach"""
try:
# Increment timer counter
timer.start_time += 1 # 1 second per check
items_to_check = []
for download_info in self.active_downloads[:]: # Iterate over a copy
items_to_check.append({
'widget_id': download_info['download_index'],
'download_id': download_info['download_id'],
'file_path': download_info['slskd_result'].filename,
'status': 'downloading',
})
if not items_to_check:
self._is_status_update_running = False
return
worker = SyncStatusProcessingWorker(
soulseek_client=self.parent_page.soulseek_client,
download_items_data=items_to_check
)
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 _handle_processed_status_updates(self, results):
"""
This runs on the main thread and applies status updates from the background worker.
This is where the retry logic is triggered.
"""
for result in results:
download_index = result['widget_id']
new_status = result['status']
# Check if this download was already completed
if (hasattr(self, 'parallel_download_tracking') and
timer.download_index in self.parallel_download_tracking and
self.parallel_download_tracking[timer.download_index]['completed']):
timer.stop()
return
download_found_in_api = False
# Step 1: Try to find download in slskd API
if hasattr(self.parent_page, 'soulseek_client') and self.parent_page.soulseek_client:
try:
all_downloads = self.parent_page.soulseek_client.get_all_downloads()
# Find our download by ID
for download in all_downloads:
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
tracking_info = self.parallel_download_tracking.get(timer.download_index, {})
last_state = tracking_info.get('last_seen_state')
if last_state == state:
# Same state as last check - increment stuck counter
tracking_info['stuck_state_count'] = tracking_info.get('stuck_state_count', 0) + 1
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":
print(f"✅ Parallel download {timer.download_index + 1} completed via API")
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:
print(f"⚠️ API error checking parallel download {timer.download_index + 1}: {api_error}")
# Continue to Transfer folder check
# Step 2: If not found in API, check Transfer folder (likely completed & cleaned up)
if not download_found_in_api:
print(f"🔍 Parallel download {timer.download_index + 1} not in API - checking Transfer folder...")
download_info = next((d for d in self.active_downloads if d['download_index'] == download_index), None)
if not download_info:
continue
if result.get('transfer_id'):
download_info['download_id'] = result['transfer_id']
if new_status in ['failed', 'cancelled']:
print(f"Track {download_index} failed/cancelled. Triggering retry...")
self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("🔄 Retrying..."))
if self.check_transfer_folder_for_parallel_download(timer.download_index):
print(f"✅ Parallel download {timer.download_index + 1} found in Transfer folder")
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
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")
# Remove from active list so we don't check it again
self.active_downloads.remove(download_info)
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))
# This is the crucial part: call your retry logic
self.on_parallel_track_failed(download_index, f"Download {new_status}")
self.retry_parallel_download_with_fallback(download_index, f"Download {new_status}")
elif new_status == 'completed':
print(f"Track {download_index} completed successfully.")
self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("✅ Downloaded"))
# Remove from active downloads list
self.active_downloads.remove(download_info)
# Signal completion to the parallel download manager
self.on_parallel_track_completed(download_index, success=True)
elif new_status == 'downloading':
progress = result.get('progress', 0)
self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem(f"⏬ Downloading ({progress}%)"))
elif new_status == 'queued':
self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("... Queued"))
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: