better
This commit is contained in:
parent
299e968522
commit
183a1a623c
4 changed files with 8648 additions and 133 deletions
|
|
@ -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"}
|
||||||
8455
logs/app.log
8455
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
324
ui/pages/sync.py
324
ui/pages/sync.py
|
|
@ -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
|
||||||
|
|
@ -1887,11 +1980,23 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
# Worker tracking
|
# Worker tracking
|
||||||
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")
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Set up the enhanced modal UI"""
|
"""Set up the enhanced modal UI"""
|
||||||
self.setWindowTitle(f"Download Missing Tracks - {self.playlist.name}")
|
self.setWindowTitle(f"Download Missing Tracks - {self.playlist.name}")
|
||||||
|
|
@ -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,135 +5286,83 @@ 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
|
||||||
|
|
||||||
|
self._is_status_update_running = True
|
||||||
|
|
||||||
# Store download tracking info
|
items_to_check = []
|
||||||
if not hasattr(self, 'parallel_download_tracking'):
|
for download_info in self.active_downloads[:]: # Iterate over a copy
|
||||||
self.parallel_download_tracking = {}
|
items_to_check.append({
|
||||||
|
'widget_id': download_info['download_index'],
|
||||||
self.parallel_download_tracking[download_index] = {
|
'download_id': download_info['download_id'],
|
||||||
'download_id': download_item.download_id,
|
'file_path': download_info['slskd_result'].filename,
|
||||||
'track_index': track_index,
|
'status': 'downloading',
|
||||||
'table_index': table_index,
|
})
|
||||||
'download_index': download_index,
|
|
||||||
'completed': False,
|
if not items_to_check:
|
||||||
'last_seen_state': None,
|
self._is_status_update_running = False
|
||||||
'stuck_state_count': 0 # Track how long it's been in same state
|
return
|
||||||
}
|
|
||||||
|
worker = SyncStatusProcessingWorker(
|
||||||
# Create timer to check download status via slskd API (same as regular downloads)
|
soulseek_client=self.parent_page.soulseek_client,
|
||||||
timer = QTimer()
|
download_items_data=items_to_check
|
||||||
timer.download_id = download_item.download_id
|
)
|
||||||
timer.track_index = track_index
|
worker.signals.completed.connect(self._handle_processed_status_updates)
|
||||||
timer.table_index = table_index
|
worker.signals.error.connect(lambda e: print(f"Status Worker Error: {e}"))
|
||||||
timer.download_index = download_index
|
self.download_status_pool.start(worker)
|
||||||
timer.start_time = 0
|
|
||||||
timer.timeout.connect(lambda: self.check_download_status_parallel(timer))
|
def _handle_processed_status_updates(self, results):
|
||||||
timer.start(1000) # Check every 1 second for faster error detection
|
"""
|
||||||
|
This runs on the main thread and applies status updates from the background worker.
|
||||||
# Store timer reference for cleanup
|
This is where the retry logic is triggered.
|
||||||
if not hasattr(self, 'download_timers'):
|
"""
|
||||||
self.download_timers = []
|
for result in results:
|
||||||
self.download_timers.append(timer)
|
download_index = result['widget_id']
|
||||||
|
new_status = result['status']
|
||||||
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
|
|
||||||
|
|
||||||
# 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()
|
if result.get('transfer_id'):
|
||||||
return
|
download_info['download_id'] = result['transfer_id']
|
||||||
|
|
||||||
download_found_in_api = False
|
if new_status in ['failed', 'cancelled']:
|
||||||
|
print(f"Track {download_index} failed/cancelled. Triggering retry...")
|
||||||
# Step 1: Try to find download in slskd API
|
self.track_table.setItem(download_info['table_index'], 4, QTableWidgetItem("🔄 Retrying..."))
|
||||||
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...")
|
|
||||||
|
|
||||||
if self.check_transfer_folder_for_parallel_download(timer.download_index):
|
# Remove from active list so we don't check it again
|
||||||
print(f"✅ Parallel download {timer.download_index + 1} found in Transfer folder")
|
self.active_downloads.remove(download_info)
|
||||||
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")
|
|
||||||
|
|
||||||
except Exception as e:
|
# This is the crucial part: call your retry logic
|
||||||
print(f"❌ Error checking parallel download status: {str(e)}")
|
self.on_parallel_track_failed(download_index, f"Download {new_status}")
|
||||||
timer.stop()
|
self.retry_parallel_download_with_fallback(download_index, f"Download {new_status}")
|
||||||
self.on_parallel_track_failed(timer.download_index, str(e))
|
|
||||||
|
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):
|
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"""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue