all is well
This commit is contained in:
parent
26256b3943
commit
96b170a3db
5 changed files with 7218 additions and 86 deletions
|
|
@ -1 +1 @@
|
|||
{"access_token": "BQACgxKQIrJWBwEQEQ1kwxCrDyB6lxj6FPjtd2eL4wVn2t1D8V2KFYIa4xPVkkQpPwZji4uklrQZdCtqi7wemG4SYpodlw76_YHgshavn_Xv6QguvIC2tSPzukdELDI3nX0jv9VUqANlfTPcj2hRBuBgWtCKoN5AZFxl7omx629HQgzjoR8MDucQoiwhs4X_suAQw3sgZHee69nKhC10AjnAs4g0BX_sV857F4ao6dJGM0Lm9hvNakjlHHVzQWWM", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752357505, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
{"access_token": "BQDEcVCtXRdYMbhowSOp7yVxA1tan4UA_VDTrCvlOqkN_EH_jzAsfKTT4dx42dBLmqT6DFJfcDqY-tcA4z0UaKEcXv0eMpLfYPgLcMbcA0TMpnnZZEn3HWXAhzjrK7tSpnheZPBWoa7OOiMC60ILTP_qhWHoBlOe3u8ueviQedlUNHo2gnlrk-pHZRaanQYUkRwaIwvOjznotLt1Br3REVJtfavBUpXRB3VHvrBbSAGqSpTgYitoElvOeCiifBJ6", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752365178, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
6847
logs/app.log
6847
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
|
|
@ -2343,37 +2343,41 @@ class DownloadItem(QFrame):
|
|||
if not self.soulseek_client or not self.download_id:
|
||||
print(f"Cannot cancel download: missing client or download ID")
|
||||
return
|
||||
|
||||
try:
|
||||
# Use async cancellation in a simple way
|
||||
import asyncio
|
||||
loop = None
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Run the cancellation
|
||||
result = loop.run_until_complete(self.soulseek_client.cancel_download(self.download_id))
|
||||
|
||||
if result:
|
||||
print(f"Successfully cancelled download: {self.title}")
|
||||
self.update_status("cancelled", progress=0)
|
||||
|
||||
# Find the parent TabbedDownloadManager and move to finished tab
|
||||
parent_widget = self.parent()
|
||||
while parent_widget:
|
||||
if hasattr(parent_widget, 'move_to_finished'):
|
||||
parent_widget.move_to_finished(self)
|
||||
break
|
||||
parent_widget = parent_widget.parent()
|
||||
|
||||
# Find the parent DownloadsPage to use its async helper
|
||||
parent_page = self.parent()
|
||||
while parent_page and not hasattr(parent_page, '_run_async_operation'):
|
||||
parent_page = parent_page.parent()
|
||||
|
||||
if parent_page:
|
||||
# Use the parent's async helper for safe event loop management
|
||||
def on_success(result):
|
||||
if result:
|
||||
print(f"Successfully cancelled download: {self.title}")
|
||||
self.update_status("cancelled", progress=0)
|
||||
|
||||
else:
|
||||
print(f"Failed to cancel download: {self.title}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error cancelling download {self.title}: {e}")
|
||||
# Find the parent TabbedDownloadManager and move to finished tab
|
||||
parent_widget = self.parent()
|
||||
while parent_widget:
|
||||
if hasattr(parent_widget, 'move_to_finished'):
|
||||
parent_widget.move_to_finished(self)
|
||||
break
|
||||
parent_widget = parent_widget.parent()
|
||||
|
||||
else:
|
||||
print(f"Failed to cancel download: {self.title}")
|
||||
|
||||
def on_error(error):
|
||||
print(f"Error cancelling download {self.title}: {error}")
|
||||
|
||||
parent_page._run_async_operation(
|
||||
self.soulseek_client.cancel_download,
|
||||
self.download_id,
|
||||
success_callback=on_success,
|
||||
error_callback=on_error
|
||||
)
|
||||
else:
|
||||
print(f"[ERROR] Could not find parent DownloadsPage for async operation")
|
||||
|
||||
def retry_download(self):
|
||||
"""Retry a failed download"""
|
||||
|
|
@ -2662,28 +2666,33 @@ class CompactDownloadItem(QFrame):
|
|||
print(f"[DEBUG] Cancel button clicked - download_id: {self.download_id}, username: {self.username}, title: {self.title}")
|
||||
if self.soulseek_client and self.download_id:
|
||||
print(f"🚫 Cancelling download: {self.download_id}")
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
# Find the parent DownloadsPage to use its async helper
|
||||
parent_page = self.parent()
|
||||
while parent_page and not hasattr(parent_page, '_run_async_operation'):
|
||||
parent_page = parent_page.parent()
|
||||
|
||||
if parent_page:
|
||||
# Use the parent's async helper for safe event loop management
|
||||
def on_success(result):
|
||||
print(f"[DEBUG] Cancel result: {result}")
|
||||
if result:
|
||||
print(f"✅ Successfully cancelled download: {self.title}")
|
||||
self.update_status("cancelled")
|
||||
else:
|
||||
print(f"❌ Failed to cancel download: {self.title}")
|
||||
|
||||
# Get or create event loop for async operation
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
def on_error(error):
|
||||
print(f"❌ Failed to cancel download: {error}")
|
||||
|
||||
# Run the cancellation with username
|
||||
result = loop.run_until_complete(self.soulseek_client.cancel_download(self.download_id, self.username))
|
||||
print(f"[DEBUG] Cancel result: {result}")
|
||||
|
||||
if result:
|
||||
print(f"✅ Successfully cancelled download: {self.title}")
|
||||
self.update_status("cancelled")
|
||||
else:
|
||||
print(f"❌ Failed to cancel download: {self.title}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to cancel download: {e}")
|
||||
parent_page._run_async_operation(
|
||||
self.soulseek_client.cancel_download,
|
||||
self.download_id, self.username,
|
||||
success_callback=on_success,
|
||||
error_callback=on_error
|
||||
)
|
||||
else:
|
||||
print(f"[ERROR] Could not find parent DownloadsPage for async operation")
|
||||
else:
|
||||
print(f"[DEBUG] Cancel failed - soulseek_client: {self.soulseek_client}, download_id: {self.download_id}")
|
||||
|
||||
|
|
@ -2911,32 +2920,71 @@ class DownloadQueue(QFrame):
|
|||
|
||||
def remove_download_item(self, item):
|
||||
"""Remove a download item from the queue"""
|
||||
print(f"[DEBUG] remove_download_item() called for '{item.title}' with status '{item.status}'")
|
||||
print(f"[DEBUG] Queue has {len(self.download_items)} items before removal")
|
||||
|
||||
if item in self.download_items:
|
||||
print(f"[DEBUG] Item found in download_items list, removing...")
|
||||
self.download_items.remove(item)
|
||||
print(f"[DEBUG] Removed from download_items list. New count: {len(self.download_items)}")
|
||||
|
||||
print(f"[DEBUG] Removing widget from queue_layout...")
|
||||
self.queue_layout.removeWidget(item)
|
||||
print(f"[DEBUG] Scheduling widget deletion...")
|
||||
item.deleteLater()
|
||||
|
||||
print(f"[DEBUG] Updating queue count...")
|
||||
self.update_queue_count()
|
||||
|
||||
# Notify parent download manager to update tab counts
|
||||
print(f"[DEBUG] Finding parent to update tab counts...")
|
||||
parent_widget = self.parent()
|
||||
while parent_widget and not hasattr(parent_widget, 'update_tab_counts'):
|
||||
parent_widget = parent_widget.parent()
|
||||
if parent_widget and hasattr(parent_widget, 'update_tab_counts'):
|
||||
print(f"[DEBUG] Calling parent.update_tab_counts()...")
|
||||
parent_widget.update_tab_counts()
|
||||
else:
|
||||
print(f"[DEBUG] No parent with update_tab_counts found")
|
||||
|
||||
print(f"[DEBUG] remove_download_item() completed for '{item.title}'")
|
||||
else:
|
||||
print(f"[DEBUG] Item '{item.title}' NOT found in download_items list!")
|
||||
|
||||
def clear_completed_downloads(self):
|
||||
"""Remove all completed and cancelled download items"""
|
||||
print(f"[DEBUG] DownloadQueue.clear_completed_downloads() called with {len(self.download_items)} items")
|
||||
items_to_remove = []
|
||||
for item in self.download_items:
|
||||
# Check for various completed and cancelled status formats
|
||||
if (item.status.lower() in ["completed", "finished", "cancelled", "canceled", "failed"] or
|
||||
item.status.lower().startswith("completed") or
|
||||
item.status.lower().startswith("cancelled") or
|
||||
item.status.lower().startswith("canceled")):
|
||||
items_to_remove.append(item)
|
||||
|
||||
for item in self.download_items:
|
||||
print(f"[DEBUG] Checking item '{item.title}' with status '{item.status}'")
|
||||
|
||||
# Normalize status for comparison (handle compound statuses like "Completed, Succeeded")
|
||||
status_lower = item.status.lower()
|
||||
should_remove = False
|
||||
|
||||
# Check for exact matches
|
||||
if status_lower in ["completed", "finished", "cancelled", "canceled", "failed"]:
|
||||
should_remove = True
|
||||
print(f"[DEBUG] Exact status match: '{item.status}'")
|
||||
|
||||
# Check for partial matches (handles compound statuses)
|
||||
elif any(keyword in status_lower for keyword in ["completed", "finished", "cancelled", "canceled", "failed", "succeeded"]):
|
||||
should_remove = True
|
||||
print(f"[DEBUG] Partial status match: '{item.status}'")
|
||||
|
||||
if should_remove:
|
||||
print(f"[DEBUG] Item '{item.title}' marked for removal (status: '{item.status}')")
|
||||
items_to_remove.append(item)
|
||||
else:
|
||||
print(f"[DEBUG] Item '{item.title}' NOT marked for removal (status: '{item.status}')")
|
||||
|
||||
print(f"[DEBUG] Removing {len(items_to_remove)} items from queue")
|
||||
for item in items_to_remove:
|
||||
print(f"[DEBUG] Removing item: '{item.title}'")
|
||||
self.remove_download_item(item)
|
||||
|
||||
print(f"[DEBUG] DownloadQueue.clear_completed_downloads() finished. Remaining items: {len(self.download_items)}")
|
||||
|
||||
class TabbedDownloadManager(QTabWidget):
|
||||
"""Tabbed interface for managing active and finished downloads"""
|
||||
|
|
@ -3007,8 +3055,12 @@ class TabbedDownloadManager(QTabWidget):
|
|||
|
||||
def move_to_finished(self, download_item):
|
||||
"""Move a download item from active to finished queue"""
|
||||
print(f"[DEBUG] move_to_finished() called for '{download_item.title}' with status '{download_item.status}'")
|
||||
print(f"[DEBUG] Finished queue currently has {len(self.finished_queue.download_items)} items")
|
||||
|
||||
if download_item in self.active_queue.download_items:
|
||||
# Remove from active queue
|
||||
print(f"[DEBUG] Removing '{download_item.title}' from active queue...")
|
||||
self.active_queue.remove_download_item(download_item)
|
||||
|
||||
# Ensure completed downloads have 100% progress
|
||||
|
|
@ -3018,6 +3070,7 @@ class TabbedDownloadManager(QTabWidget):
|
|||
print(f"[DEBUG] Ensuring completed download '{download_item.title}' has 100% progress")
|
||||
|
||||
# Add to finished queue
|
||||
print(f"[DEBUG] Adding '{download_item.title}' to finished queue with status '{download_item.status}'...")
|
||||
finished_item = self.finished_queue.add_download_item(
|
||||
title=download_item.title,
|
||||
artist=download_item.artist,
|
||||
|
|
@ -3030,6 +3083,7 @@ class TabbedDownloadManager(QTabWidget):
|
|||
username=download_item.username,
|
||||
soulseek_client=download_item.soulseek_client
|
||||
)
|
||||
print(f"[DEBUG] Finished queue now has {len(self.finished_queue.download_items)} items")
|
||||
|
||||
self.update_tab_counts()
|
||||
return finished_item
|
||||
|
|
@ -3068,6 +3122,22 @@ class TabbedDownloadManager(QTabWidget):
|
|||
self.finished_queue.clear_completed_downloads()
|
||||
self.update_tab_counts()
|
||||
|
||||
def clear_local_queues_only(self):
|
||||
"""Clear only the local UI queues without backend operations (for use by parent)"""
|
||||
print("[DEBUG] TabbedDownloadManager.clear_local_queues_only() called")
|
||||
print(f"[DEBUG] Active queue has {len(self.active_queue.download_items)} items")
|
||||
print(f"[DEBUG] Finished queue has {len(self.finished_queue.download_items)} items")
|
||||
|
||||
# Clear from both active and finished queues
|
||||
print("[DEBUG] Clearing active queue...")
|
||||
self.active_queue.clear_completed_downloads()
|
||||
print("[DEBUG] Clearing finished queue...")
|
||||
self.finished_queue.clear_completed_downloads()
|
||||
print("[DEBUG] Updating tab counts...")
|
||||
self.update_tab_counts()
|
||||
|
||||
print(f"[DEBUG] After clearing - Active: {len(self.active_queue.download_items)}, Finished: {len(self.finished_queue.download_items)}")
|
||||
|
||||
@property
|
||||
def download_items(self):
|
||||
"""Return all download items from active queue for compatibility"""
|
||||
|
|
@ -3085,6 +3155,9 @@ class DownloadsPage(QWidget):
|
|||
track_loading_finished = pyqtSignal(object) # Track result object when streaming completes
|
||||
track_loading_progress = pyqtSignal(float, object) # Progress percentage (0-100), track result object
|
||||
|
||||
# Signal for clear completed downloads completion (thread-safe communication)
|
||||
clear_completed_finished = pyqtSignal(bool, object) # backend_success, ui_callback
|
||||
|
||||
def __init__(self, soulseek_client=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.soulseek_client = soulseek_client
|
||||
|
|
@ -3112,6 +3185,8 @@ class DownloadsPage(QWidget):
|
|||
self.download_status_timer.timeout.connect(self.update_download_status)
|
||||
self.download_status_timer.start(2000) # Poll every 2 seconds
|
||||
|
||||
# Connect clear completed signal for thread-safe communication
|
||||
self.clear_completed_finished.connect(self._handle_clear_completion)
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
|
|
@ -4814,11 +4889,18 @@ class DownloadsPage(QWidget):
|
|||
print(f"🛑 Stopping streaming thread for: {getattr(thread.search_result, 'filename', 'unknown')}")
|
||||
thread.stop() # Request stop
|
||||
|
||||
# Give thread a moment to stop gracefully
|
||||
if not thread.wait(1000): # Wait up to 1 second
|
||||
print(f"⚠️ Force terminating streaming thread")
|
||||
thread.terminate()
|
||||
thread.wait(1000) # Wait for termination
|
||||
# Give thread more time to stop gracefully (3 seconds)
|
||||
if not thread.wait(3000): # Wait up to 3 seconds
|
||||
print(f"⚠️ Streaming thread taking longer to stop, giving more time...")
|
||||
# Try one more time with longer wait
|
||||
if not thread.wait(2000): # Additional 2 seconds
|
||||
print(f"⚠️ Force terminating unresponsive streaming thread")
|
||||
thread.terminate()
|
||||
thread.wait(1000) # Wait for termination
|
||||
else:
|
||||
print(f"✓ Streaming thread stopped gracefully (delayed)")
|
||||
else:
|
||||
print(f"✓ Streaming thread stopped gracefully")
|
||||
|
||||
# Remove from list
|
||||
if thread in self.streaming_threads:
|
||||
|
|
@ -4859,20 +4941,34 @@ class DownloadsPage(QWidget):
|
|||
|
||||
if download_id:
|
||||
print(f"🚫 Found streaming download ID: {download_id}")
|
||||
# Cancel the download with remove=True to clean it up
|
||||
success = await self.soulseek_client.cancel_download(download_id, username, remove=True)
|
||||
# Cancel the download with remove=False (slskd won't allow remove=True for active downloads)
|
||||
success = await self.soulseek_client.cancel_download(download_id, username, remove=False)
|
||||
if success:
|
||||
print(f"✓ Successfully cancelled streaming download: {os.path.basename(filename)}")
|
||||
else:
|
||||
print(f"⚠️ Failed to cancel streaming download: {os.path.basename(filename)}")
|
||||
# Try without remove flag as fallback
|
||||
try:
|
||||
success = await self.soulseek_client.cancel_download(download_id, username, remove=False)
|
||||
if success:
|
||||
print(f"✓ Cancelled streaming download with fallback method: {os.path.basename(filename)}")
|
||||
except Exception as fallback_e:
|
||||
print(f"⚠️ Fallback cancellation also failed: {fallback_e}")
|
||||
else:
|
||||
print(f"⚠️ Could not find download ID for streaming download: {os.path.basename(filename)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error cancelling streaming download: {e}")
|
||||
# Continue with graceful fallback - don't let cancellation errors break streaming
|
||||
print(f"🔄 Continuing with new stream despite cancellation error")
|
||||
finally:
|
||||
# Clear tracking regardless of success
|
||||
# Clean up any partial files from the cancelled streaming download
|
||||
if hasattr(self, 'current_streaming_download') and self.current_streaming_download:
|
||||
await self._cleanup_cancelled_streaming_files(self.current_streaming_download)
|
||||
|
||||
# Clear tracking regardless of success to prevent stuck state
|
||||
self.current_streaming_download = None
|
||||
print(f"🧹 Cleared streaming download tracking")
|
||||
|
||||
# Also clean up any completed streaming downloads to prevent queue clogging
|
||||
await self._cleanup_completed_streaming_downloads()
|
||||
|
|
@ -4931,18 +5027,88 @@ class DownloadsPage(QWidget):
|
|||
except Exception as e:
|
||||
print(f"⚠️ Error during streaming download cleanup: {e}")
|
||||
|
||||
async def _cleanup_cancelled_streaming_files(self, download_info):
|
||||
"""Clean up partial files from cancelled streaming downloads"""
|
||||
try:
|
||||
username = download_info.get('username', '')
|
||||
filename = download_info.get('filename', '')
|
||||
|
||||
if not username or not filename:
|
||||
return
|
||||
|
||||
print(f"🧹 Cleaning up cancelled streaming files for: {os.path.basename(filename)}")
|
||||
|
||||
# Get downloads directory from config
|
||||
from config.settings import config_manager
|
||||
downloads_config = config_manager.get_downloads_config()
|
||||
download_path = downloads_config.get('path', './downloads')
|
||||
|
||||
# Look for partial/completed files in downloads directory
|
||||
filename_base = os.path.splitext(os.path.basename(filename))[0]
|
||||
|
||||
# Search for files that might match this download
|
||||
for root, dirs, files in os.walk(download_path):
|
||||
for file in files:
|
||||
# Check if this file could be from our cancelled download
|
||||
if (filename_base.lower() in file.lower() or
|
||||
os.path.basename(filename).lower() == file.lower()):
|
||||
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
print(f"🗑️ Removing cancelled streaming file: {file_path}")
|
||||
os.remove(file_path)
|
||||
|
||||
# Clean up empty directories
|
||||
self._cleanup_empty_directories(download_path, file_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error removing file {file_path}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error cleaning up cancelled streaming files: {e}")
|
||||
|
||||
def _cancel_current_streaming_download_sync(self):
|
||||
"""Synchronous wrapper for cancelling current streaming download"""
|
||||
if hasattr(self, 'current_streaming_download') and self.current_streaming_download:
|
||||
# Use async event loop to run the cancellation
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
try:
|
||||
# Try to get existing event loop first
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
# If loop is running, we need to run in a thread
|
||||
def run_in_thread():
|
||||
new_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(new_loop)
|
||||
try:
|
||||
new_loop.run_until_complete(self._cancel_current_streaming_download())
|
||||
finally:
|
||||
new_loop.close()
|
||||
|
||||
thread = threading.Thread(target=run_in_thread)
|
||||
thread.start()
|
||||
thread.join(timeout=5.0) # Wait max 5 seconds
|
||||
return
|
||||
except RuntimeError:
|
||||
# No event loop in current thread
|
||||
pass
|
||||
|
||||
# Create and use new event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self._cancel_current_streaming_download())
|
||||
loop.close()
|
||||
try:
|
||||
loop.run_until_complete(self._cancel_current_streaming_download())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in sync streaming download cancellation: {e}")
|
||||
print(f"🔄 Continuing with new stream despite sync cancellation error")
|
||||
# Clear tracking as fallback to prevent stuck state
|
||||
self.current_streaming_download = None
|
||||
|
||||
def on_streaming_thread_finished(self, thread):
|
||||
"""Clean up when streaming thread finishes"""
|
||||
|
|
@ -5168,36 +5334,154 @@ class DownloadsPage(QWidget):
|
|||
except Exception as e:
|
||||
print(f"Error cleaning up finished download thread: {e}")
|
||||
|
||||
def _run_async_operation(self, async_func, *args, success_callback=None, error_callback=None):
|
||||
"""Helper method to run async operations safely with proper event loop management"""
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
def run_operation():
|
||||
"""Run the async operation in a separate thread with its own event loop"""
|
||||
try:
|
||||
# Create a fresh event loop for this thread
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Run the async operation
|
||||
result = loop.run_until_complete(async_func(*args))
|
||||
|
||||
# Schedule success callback on main thread if provided
|
||||
if success_callback:
|
||||
QTimer.singleShot(0, lambda: success_callback(result))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception in async operation: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Schedule error callback on main thread if provided
|
||||
if error_callback:
|
||||
# Capture the error in a closure to avoid lambda variable issues
|
||||
def call_error_callback(error=e):
|
||||
error_callback(error)
|
||||
QTimer.singleShot(0, call_error_callback)
|
||||
|
||||
return False
|
||||
|
||||
finally:
|
||||
# Always close the loop we created
|
||||
try:
|
||||
loop.close()
|
||||
except Exception as close_e:
|
||||
print(f"[WARNING] Error closing event loop: {close_e}")
|
||||
|
||||
try:
|
||||
# Run the operation in a separate thread
|
||||
operation_thread = threading.Thread(target=run_operation, daemon=True)
|
||||
operation_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception starting async operation thread: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def clear_completed_downloads(self):
|
||||
"""Clear completed and cancelled downloads from both slskd backend and local queues"""
|
||||
print("[DEBUG] DownloadsPage.clear_completed_downloads() method called!")
|
||||
print(f"[DEBUG] Current download queue stats:")
|
||||
print(f"[DEBUG] - Active queue: {len(self.download_queue.active_queue.download_items)} items")
|
||||
print(f"[DEBUG] - Finished queue: {len(self.download_queue.finished_queue.download_items)} items")
|
||||
|
||||
if not self.soulseek_client:
|
||||
print("[ERROR] No soulseek client available for clearing downloads")
|
||||
return
|
||||
|
||||
# Run async clear operation
|
||||
# Run async clear operation using threading to avoid event loop conflicts
|
||||
import asyncio
|
||||
try:
|
||||
# Get the current event loop or create a new one
|
||||
import threading
|
||||
|
||||
# Define UI update callback outside the thread (with proper self reference)
|
||||
def update_ui_callback():
|
||||
"""UI update callback that runs on main thread"""
|
||||
print("[DEBUG] *** UI CALLBACK EXECUTED *** - Starting UI clear operations...")
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
# Step 1: Clear local queues
|
||||
print("[DEBUG] Step 1: Calling clear_local_queues_only()...")
|
||||
self.download_queue.clear_local_queues_only()
|
||||
print("[DEBUG] Step 1 completed successfully")
|
||||
|
||||
# Step 2: Update download status
|
||||
print("[DEBUG] Step 2: Calling update_download_status()...")
|
||||
self.update_download_status()
|
||||
print("[DEBUG] Step 2 completed successfully")
|
||||
|
||||
print("[DEBUG] *** UI CALLBACK COMPLETED *** - All UI clear operations finished")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception in UI callback: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Even if there's an error, try to update the display
|
||||
try:
|
||||
print("[DEBUG] Attempting fallback queue update...")
|
||||
if hasattr(self, 'download_queue'):
|
||||
self.download_queue.update_tab_counts()
|
||||
except Exception as fallback_e:
|
||||
print(f"[ERROR] Fallback update also failed: {fallback_e}")
|
||||
|
||||
def run_clear_operation():
|
||||
"""Run the clear operation in a separate thread with its own event loop"""
|
||||
success = False
|
||||
try:
|
||||
# Create a fresh event loop for this thread
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
print("[DEBUG] 🗑️ Clearing all completed/cancelled downloads from slskd backend...")
|
||||
success = loop.run_until_complete(self.soulseek_client.clear_all_completed_downloads())
|
||||
|
||||
if success:
|
||||
print("[DEBUG] ✅ Successfully cleared completed/cancelled downloads from backend")
|
||||
# Also clear from local UI (both active and finished queues)
|
||||
self.download_queue.clear_completed_downloads()
|
||||
# Trigger immediate status update to refresh UI
|
||||
self.update_download_status()
|
||||
else:
|
||||
print("[ERROR] ❌ Failed to clear completed/cancelled downloads from backend")
|
||||
|
||||
print("[DEBUG] 🗑️ Clearing all completed/cancelled downloads from slskd backend...")
|
||||
success = loop.run_until_complete(self.soulseek_client.clear_all_completed_downloads())
|
||||
|
||||
if success:
|
||||
print("[DEBUG] ✅ Successfully cleared completed/cancelled downloads from backend")
|
||||
else:
|
||||
print("[WARNING] ❌ Backend reported failure, but proceeding with UI clearing anyway")
|
||||
print("[WARNING] (Web UI may have cleared successfully despite backend failure report)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception during clear completed downloads: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# Always close the loop we created
|
||||
try:
|
||||
loop.close()
|
||||
except Exception as close_e:
|
||||
print(f"[WARNING] Error closing event loop: {close_e}")
|
||||
|
||||
# CRITICAL: Use signal to communicate with main thread (thread-safe)
|
||||
print("[DEBUG] Thread completed, emitting completion signal...")
|
||||
self.clear_completed_finished.emit(success, update_ui_callback)
|
||||
|
||||
try:
|
||||
# Run the clear operation in a separate thread
|
||||
clear_thread = threading.Thread(target=run_clear_operation, daemon=True)
|
||||
clear_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception during clear completed downloads: {e}")
|
||||
print(f"[ERROR] Exception starting clear completed downloads thread: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _handle_clear_completion(self, backend_success, ui_callback):
|
||||
"""Handle completion of clear operation on main thread"""
|
||||
print(f"[DEBUG] _handle_clear_completion called on main thread - backend_success: {backend_success}")
|
||||
|
||||
# ALWAYS clear UI regardless of backend success/failure
|
||||
# This ensures UI stays in sync even if backend reports false negatives
|
||||
print("[DEBUG] Executing UI callback on main thread...")
|
||||
try:
|
||||
ui_callback()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception executing UI callback: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
|
@ -5612,6 +5896,7 @@ class DownloadsPage(QWidget):
|
|||
# Clear completed button
|
||||
clear_btn = QPushButton("🗑️ Clear Completed")
|
||||
clear_btn.setFixedHeight(35)
|
||||
clear_btn.clicked.connect(self.clear_completed_downloads) # Connect to the clearing method
|
||||
clear_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
|
|
|
|||
Loading…
Reference in a new issue