progress
This commit is contained in:
parent
106f5178a2
commit
18f2cac879
8 changed files with 4176 additions and 25 deletions
|
|
@ -1 +1 @@
|
||||||
{"access_token": "BQAjMPjb13VCQ73SsbWrU9xW8jJmPaJScM6MpLge7nblR7Uve24yXsjltxc-n0PbMp5PENCcLvpJfRqEV627LcUPfTsOwbKyNiEz37DxIwWRiuUEbgZtWznOBF0V0D6ukIS_GPwl1kGmmzpD_nl088_oKB4SmZS4HEKntNvDgoG90H9TAxRJDNHqD3pF7rzVzPKubsML9mv-0g5TgajaiiPbqowaOUYCzVskJSiNdZPAmZsV2iiGvZ2ugI3uQOAt", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752276843, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
{"access_token": "BQC2H3Gtur2UVBolbGlrlVtwEq7r-0GaGWTmBQZERyH-UVWKOOu8g2nDf-pKwL7w2yf5j9uolii2YBXpEoCOZyaa2rVbLbzbERDfw__hJ9JDhxwWT7OuIf3gxxCPkZs9d7UEaonxyPY7B8bLh49_uhvYpq7azOAJE40bPknaHR230KGsLQpqQbdo4In07iCLaD-PFGYHC4Me7c0QRvyTC1-gx9qgfvlDF-BvIEHsBpDW6dibzSO0AQjzaR4-Okb-", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752286148, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -254,7 +254,7 @@ class SoulseekClient:
|
||||||
logger.debug(f"Response status: {response.status}")
|
logger.debug(f"Response status: {response.status}")
|
||||||
logger.debug(f"Response text: {response_text[:500]}...") # First 500 chars
|
logger.debug(f"Response text: {response_text[:500]}...") # First 500 chars
|
||||||
|
|
||||||
if response.status in [200, 201]: # Accept both 200 OK and 201 Created
|
if response.status in [200, 201, 204]: # Accept 200 OK, 201 Created, and 204 No Content
|
||||||
try:
|
try:
|
||||||
if response_text.strip(): # Only parse if there's content
|
if response_text.strip(): # Only parse if there's content
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
@ -836,18 +836,72 @@ class SoulseekClient:
|
||||||
logger.error(f"Error getting downloads: {e}")
|
logger.error(f"Error getting downloads: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def cancel_download(self, download_id: str) -> bool:
|
async def cancel_download(self, download_id: str, username: str = None, remove: bool = False) -> bool:
|
||||||
if not self.base_url:
|
if not self.base_url:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# If username is not provided, try to extract it from stored transfer data
|
||||||
|
if not username:
|
||||||
|
logger.debug(f"No username provided for download_id {download_id}, attempting to find it")
|
||||||
|
try:
|
||||||
|
downloads = await self.get_all_downloads()
|
||||||
|
for download in downloads:
|
||||||
|
if download.id == download_id:
|
||||||
|
username = download.username
|
||||||
|
logger.debug(f"Found username {username} for download_id {download_id}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
logger.error(f"Could not find username for download_id {download_id}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error finding username for download: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self._make_request('DELETE', f'transfers/downloads/{download_id}')
|
# Use correct slskd API format: DELETE /transfers/downloads/{username}/{download_id}?remove={true/false}
|
||||||
|
# remove=false: Cancel download but keep in transfer list
|
||||||
|
# remove=true: Remove download completely from transfer list
|
||||||
|
endpoint = f'transfers/downloads/{username}/{download_id}?remove={str(remove).lower()}'
|
||||||
|
action = "Removing" if remove else "Cancelling"
|
||||||
|
logger.debug(f"{action} download with endpoint: {endpoint}")
|
||||||
|
response = await self._make_request('DELETE', endpoint)
|
||||||
return response is not None
|
return response is not None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error cancelling download: {e}")
|
logger.error(f"Error cancelling download: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def clear_all_completed_downloads(self) -> bool:
|
||||||
|
"""Clear all completed/finished downloads from slskd backend
|
||||||
|
|
||||||
|
Uses the /api/v0/transfers/downloads/all/completed endpoint to remove
|
||||||
|
all downloads with completed, cancelled, or failed status from slskd.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if clearing was successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.base_url:
|
||||||
|
logger.error("Soulseek client not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
endpoint = 'transfers/downloads/all/completed'
|
||||||
|
logger.debug(f"Clearing all completed downloads with endpoint: {endpoint}")
|
||||||
|
response = await self._make_request('DELETE', endpoint)
|
||||||
|
success = response is not None
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("Successfully cleared all completed downloads from slskd")
|
||||||
|
else:
|
||||||
|
logger.error("Failed to clear completed downloads from slskd")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error clearing completed downloads: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
async def search_and_download_best(self, query: str, preferred_quality: str = 'flac') -> Optional[str]:
|
async def search_and_download_best(self, query: str, preferred_quality: str = 'flac') -> Optional[str]:
|
||||||
results = await self.search(query)
|
results = await self.search(query)
|
||||||
|
|
||||||
|
|
|
||||||
3849
logs/app.log
3849
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
|
|
@ -2081,8 +2081,8 @@ class CompactDownloadItem(QFrame):
|
||||||
"""Compact download item optimized for queue display"""
|
"""Compact download item optimized for queue display"""
|
||||||
def __init__(self, title: str, artist: str, status: str = "queued",
|
def __init__(self, title: str, artist: str, status: str = "queued",
|
||||||
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
||||||
file_path: str = "", download_id: str = "", soulseek_client=None,
|
file_path: str = "", download_id: str = "", username: str = "",
|
||||||
queue_type: str = "active", parent=None):
|
soulseek_client=None, queue_type: str = "active", parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artist = artist
|
self.artist = artist
|
||||||
|
|
@ -2092,6 +2092,7 @@ class CompactDownloadItem(QFrame):
|
||||||
self.download_speed = download_speed
|
self.download_speed = download_speed
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.download_id = download_id
|
self.download_id = download_id
|
||||||
|
self.username = username
|
||||||
self.soulseek_client = soulseek_client
|
self.soulseek_client = soulseek_client
|
||||||
self.queue_type = queue_type # "active" or "finished"
|
self.queue_type = queue_type # "active" or "finished"
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
@ -2178,15 +2179,76 @@ class CompactDownloadItem(QFrame):
|
||||||
progress_layout.addWidget(self.progress_bar)
|
progress_layout.addWidget(self.progress_bar)
|
||||||
progress_layout.addWidget(self.status_label)
|
progress_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
# Right section: Action button (fixed width)
|
# Right section: Action button(s) (fixed width)
|
||||||
self.action_btn = QPushButton()
|
if self.queue_type == "finished":
|
||||||
self.action_btn.setFixedSize(70, 32) # Smaller button
|
# For finished downloads, create a container with two buttons
|
||||||
self.setup_action_button()
|
button_container = QWidget()
|
||||||
|
button_container.setFixedWidth(150) # Wider to fit two buttons
|
||||||
|
button_layout = QHBoxLayout(button_container)
|
||||||
|
button_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
button_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Open button
|
||||||
|
self.open_btn = QPushButton("📂")
|
||||||
|
self.open_btn.setFixedSize(32, 32)
|
||||||
|
self.open_btn.setToolTip("Open download folder")
|
||||||
|
self.open_btn.clicked.connect(self.open_download_location)
|
||||||
|
self.open_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(40, 167, 69, 0.8),
|
||||||
|
stop:1 rgba(40, 167, 69, 1.0));
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(20, 147, 49, 0.9),
|
||||||
|
stop:1 rgba(20, 147, 49, 1.0));
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Remove button
|
||||||
|
self.remove_btn = QPushButton("🗑️")
|
||||||
|
self.remove_btn.setFixedSize(32, 32)
|
||||||
|
self.remove_btn.setToolTip("Remove from downloads")
|
||||||
|
self.remove_btn.clicked.connect(self.remove_download)
|
||||||
|
self.remove_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(220, 53, 69, 0.8),
|
||||||
|
stop:1 rgba(220, 53, 69, 1.0));
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(200, 33, 49, 0.9),
|
||||||
|
stop:1 rgba(200, 33, 49, 1.0));
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
button_layout.addWidget(self.open_btn)
|
||||||
|
button_layout.addWidget(self.remove_btn)
|
||||||
|
|
||||||
|
action_widget = button_container
|
||||||
|
else:
|
||||||
|
# For active downloads, single action button
|
||||||
|
self.action_btn = QPushButton()
|
||||||
|
self.action_btn.setFixedSize(70, 32) # Smaller button
|
||||||
|
self.setup_action_button()
|
||||||
|
action_widget = self.action_btn
|
||||||
|
|
||||||
# Add everything to main layout
|
# Add everything to main layout
|
||||||
layout.addLayout(left_section, 1) # Flexible
|
layout.addLayout(left_section, 1) # Flexible
|
||||||
layout.addWidget(progress_widget) # Fixed width
|
layout.addWidget(progress_widget) # Fixed width
|
||||||
layout.addWidget(self.action_btn) # Fixed width
|
layout.addWidget(action_widget) # Fixed width
|
||||||
|
|
||||||
def get_display_filename(self):
|
def get_display_filename(self):
|
||||||
"""Extract just the filename with extension for display"""
|
"""Extract just the filename with extension for display"""
|
||||||
|
|
@ -2225,6 +2287,7 @@ class CompactDownloadItem(QFrame):
|
||||||
if self.status.lower() in ["downloading", "queued"]:
|
if self.status.lower() in ["downloading", "queued"]:
|
||||||
self.action_btn.setText("Cancel")
|
self.action_btn.setText("Cancel")
|
||||||
self.action_btn.clicked.connect(self.cancel_download)
|
self.action_btn.clicked.connect(self.cancel_download)
|
||||||
|
print(f"[DEBUG] Connected cancel button for: {self.title}")
|
||||||
self.action_btn.setStyleSheet("""
|
self.action_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
|
@ -2279,6 +2342,7 @@ class CompactDownloadItem(QFrame):
|
||||||
# Finished downloads: Always show Open button
|
# Finished downloads: Always show Open button
|
||||||
self.action_btn.setText("📂 Open")
|
self.action_btn.setText("📂 Open")
|
||||||
self.action_btn.clicked.connect(self.open_download_location)
|
self.action_btn.clicked.connect(self.open_download_location)
|
||||||
|
print(f"[DEBUG] Connected open button for: {self.title} (path: {self.file_path})")
|
||||||
self.action_btn.setStyleSheet("""
|
self.action_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
|
@ -2317,15 +2381,73 @@ class CompactDownloadItem(QFrame):
|
||||||
# Update action button if needed
|
# Update action button if needed
|
||||||
self.setup_action_button()
|
self.setup_action_button()
|
||||||
|
|
||||||
|
def remove_download(self):
|
||||||
|
"""Remove the download completely from transfer list"""
|
||||||
|
print(f"[DEBUG] Remove button clicked - download_id: {self.download_id}, username: {self.username}, title: {self.title}")
|
||||||
|
if self.soulseek_client and self.download_id:
|
||||||
|
print(f"🗑️ Removing download: {self.download_id}")
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Run the removal with remove=true
|
||||||
|
result = loop.run_until_complete(self.soulseek_client.cancel_download(self.download_id, self.username, remove=True))
|
||||||
|
print(f"[DEBUG] Remove result: {result}")
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"✅ Successfully removed download: {self.title}")
|
||||||
|
print(f"[DEBUG] Download should be removed from slskd - waiting for next status update to reflect removal")
|
||||||
|
# Force an immediate status update to check if it's been removed
|
||||||
|
print(f"[DEBUG] Triggering immediate status poll to detect removal")
|
||||||
|
# Find the downloads page and trigger status update
|
||||||
|
downloads_page = self
|
||||||
|
while downloads_page and not hasattr(downloads_page, 'update_download_status'):
|
||||||
|
downloads_page = downloads_page.parent()
|
||||||
|
if downloads_page:
|
||||||
|
downloads_page.update_download_status()
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to remove download: {self.title}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to remove download: {e}")
|
||||||
|
else:
|
||||||
|
print(f"[DEBUG] Remove failed - soulseek_client: {self.soulseek_client}, download_id: {self.download_id}")
|
||||||
|
|
||||||
def cancel_download(self):
|
def cancel_download(self):
|
||||||
"""Cancel the download using soulseek client"""
|
"""Cancel the download using soulseek client"""
|
||||||
|
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:
|
if self.soulseek_client and self.download_id:
|
||||||
print(f"🚫 Cancelling download: {self.download_id}")
|
print(f"🚫 Cancelling download: {self.download_id}")
|
||||||
try:
|
try:
|
||||||
self.soulseek_client.cancel_download(self.download_id)
|
import asyncio
|
||||||
self.update_status("cancelled")
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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:
|
except Exception as e:
|
||||||
print(f"❌ Failed to cancel download: {e}")
|
print(f"❌ Failed to cancel download: {e}")
|
||||||
|
else:
|
||||||
|
print(f"[DEBUG] Cancel failed - soulseek_client: {self.soulseek_client}, download_id: {self.download_id}")
|
||||||
|
|
||||||
def retry_download(self):
|
def retry_download(self):
|
||||||
"""Retry a failed download"""
|
"""Retry a failed download"""
|
||||||
|
|
@ -2340,13 +2462,35 @@ class CompactDownloadItem(QFrame):
|
||||||
import platform
|
import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
print(f"[DEBUG] Open button clicked - file_path: {self.file_path}, title: {self.title}")
|
||||||
|
|
||||||
if not self.file_path:
|
if not self.file_path:
|
||||||
|
print(f"[DEBUG] No file_path set for download: {self.title}")
|
||||||
|
# Fallback to opening the general downloads folder
|
||||||
|
try:
|
||||||
|
from config.settings import config_manager
|
||||||
|
download_path = config_manager.get('soulseek.download_path', './downloads')
|
||||||
|
|
||||||
|
system = platform.system()
|
||||||
|
if system == "Windows":
|
||||||
|
os.startfile(download_path)
|
||||||
|
elif system == "Darwin": # macOS
|
||||||
|
os.system(f'open "{download_path}"')
|
||||||
|
else: # Linux
|
||||||
|
os.system(f'xdg-open "{download_path}"')
|
||||||
|
|
||||||
|
print(f"📂 Opened downloads folder: {download_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to open downloads folder: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_path = Path(self.file_path)
|
file_path = Path(self.file_path)
|
||||||
|
print(f"[DEBUG] Checking file existence: {file_path}")
|
||||||
|
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
folder_path = file_path.parent
|
folder_path = file_path.parent
|
||||||
|
print(f"[DEBUG] Opening folder: {folder_path}")
|
||||||
|
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
|
|
@ -2359,6 +2503,50 @@ class CompactDownloadItem(QFrame):
|
||||||
print(f"📂 Opened folder: {folder_path}")
|
print(f"📂 Opened folder: {folder_path}")
|
||||||
else:
|
else:
|
||||||
print(f"❌ File not found: {file_path}")
|
print(f"❌ File not found: {file_path}")
|
||||||
|
# Try to find the file in the downloads directory using the filename
|
||||||
|
filename = os.path.basename(self.file_path)
|
||||||
|
print(f"[DEBUG] Searching for file: {filename}")
|
||||||
|
|
||||||
|
from config.settings import config_manager
|
||||||
|
download_path = config_manager.get('soulseek.download_path', './downloads')
|
||||||
|
|
||||||
|
# Search for the file in the downloads directory tree
|
||||||
|
found_file = None
|
||||||
|
for root, dirs, files in os.walk(download_path):
|
||||||
|
for file in files:
|
||||||
|
if file == filename:
|
||||||
|
found_file = os.path.join(root, file)
|
||||||
|
print(f"[DEBUG] Found file at: {found_file}")
|
||||||
|
break
|
||||||
|
if found_file:
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_file:
|
||||||
|
folder_path = os.path.dirname(found_file)
|
||||||
|
print(f"[DEBUG] Opening found folder: {folder_path}")
|
||||||
|
|
||||||
|
system = platform.system()
|
||||||
|
if system == "Windows":
|
||||||
|
os.startfile(folder_path)
|
||||||
|
elif system == "Darwin": # macOS
|
||||||
|
os.system(f'open "{folder_path}"')
|
||||||
|
else: # Linux
|
||||||
|
os.system(f'xdg-open "{folder_path}"')
|
||||||
|
|
||||||
|
print(f"📂 Opened folder: {folder_path}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Could not find file {filename} in downloads directory")
|
||||||
|
# Fallback to opening the downloads folder
|
||||||
|
system = platform.system()
|
||||||
|
if system == "Windows":
|
||||||
|
os.startfile(download_path)
|
||||||
|
elif system == "Darwin": # macOS
|
||||||
|
os.system(f'open "{download_path}"')
|
||||||
|
else: # Linux
|
||||||
|
os.system(f'xdg-open "{download_path}"')
|
||||||
|
|
||||||
|
print(f"📂 Opened downloads folder as fallback: {download_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Failed to open download location: {e}")
|
print(f"❌ Failed to open download location: {e}")
|
||||||
|
|
||||||
|
|
@ -2453,14 +2641,15 @@ class DownloadQueue(QFrame):
|
||||||
|
|
||||||
def add_download_item(self, title: str, artist: str, status: str = "queued",
|
def add_download_item(self, title: str, artist: str, status: str = "queued",
|
||||||
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
||||||
file_path: str = "", download_id: str = "", soulseek_client=None):
|
file_path: str = "", download_id: str = "", username: str = "",
|
||||||
|
soulseek_client=None):
|
||||||
"""Add a new download item to the queue"""
|
"""Add a new download item to the queue"""
|
||||||
# Hide empty message if this is the first item
|
# Hide empty message if this is the first item
|
||||||
if len(self.download_items) == 0:
|
if len(self.download_items) == 0:
|
||||||
self.empty_message.hide()
|
self.empty_message.hide()
|
||||||
|
|
||||||
# Create new compact download item with queue type
|
# Create new compact download item with queue type
|
||||||
item = CompactDownloadItem(title, artist, status, progress, file_size, download_speed, file_path, download_id, soulseek_client, self.queue_type)
|
item = CompactDownloadItem(title, artist, status, progress, file_size, download_speed, file_path, download_id, username, soulseek_client, self.queue_type)
|
||||||
self.download_items.append(item)
|
self.download_items.append(item)
|
||||||
|
|
||||||
# Insert before the stretch (which is always last)
|
# Insert before the stretch (which is always last)
|
||||||
|
|
@ -2559,11 +2748,12 @@ class TabbedDownloadManager(QTabWidget):
|
||||||
|
|
||||||
def add_download_item(self, title: str, artist: str, status: str = "queued",
|
def add_download_item(self, title: str, artist: str, status: str = "queued",
|
||||||
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
||||||
file_path: str = "", download_id: str = "", soulseek_client=None):
|
file_path: str = "", download_id: str = "", username: str = "",
|
||||||
|
soulseek_client=None):
|
||||||
"""Add a new download item to the active queue"""
|
"""Add a new download item to the active queue"""
|
||||||
item = self.active_queue.add_download_item(
|
item = self.active_queue.add_download_item(
|
||||||
title, artist, status, progress, file_size, download_speed,
|
title, artist, status, progress, file_size, download_speed,
|
||||||
file_path, download_id, soulseek_client
|
file_path, download_id, username, soulseek_client
|
||||||
)
|
)
|
||||||
self.update_tab_counts()
|
self.update_tab_counts()
|
||||||
return item
|
return item
|
||||||
|
|
@ -2590,6 +2780,7 @@ class TabbedDownloadManager(QTabWidget):
|
||||||
download_speed=download_item.download_speed,
|
download_speed=download_item.download_speed,
|
||||||
file_path=download_item.file_path,
|
file_path=download_item.file_path,
|
||||||
download_id=download_item.download_id,
|
download_id=download_item.download_id,
|
||||||
|
username=download_item.username,
|
||||||
soulseek_client=download_item.soulseek_client
|
soulseek_client=download_item.soulseek_client
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -2610,9 +2801,15 @@ class TabbedDownloadManager(QTabWidget):
|
||||||
self.parent().update_download_manager_stats(active_count, finished_count)
|
self.parent().update_download_manager_stats(active_count, finished_count)
|
||||||
|
|
||||||
def clear_completed_downloads(self):
|
def clear_completed_downloads(self):
|
||||||
"""Clear completed downloads from the finished queue"""
|
"""Clear completed downloads from both slskd backend and local queues"""
|
||||||
self.finished_queue.clear_completed_downloads()
|
# Delegate to parent (DownloadsPage) which has access to soulseek_client
|
||||||
self.update_tab_counts()
|
if hasattr(self.parent(), 'clear_completed_downloads'):
|
||||||
|
self.parent().clear_completed_downloads()
|
||||||
|
else:
|
||||||
|
# Fallback to local clearing if parent method not available
|
||||||
|
print("[DEBUG] No parent clear method found, clearing locally only")
|
||||||
|
self.finished_queue.clear_completed_downloads()
|
||||||
|
self.update_tab_counts()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def download_items(self):
|
def download_items(self):
|
||||||
|
|
@ -3823,6 +4020,7 @@ class DownloadsPage(QWidget):
|
||||||
progress=0,
|
progress=0,
|
||||||
file_size=search_result.size,
|
file_size=search_result.size,
|
||||||
download_id=download_id,
|
download_id=download_id,
|
||||||
|
username=search_result.username,
|
||||||
file_path=full_filename, # Store the full path for matching
|
file_path=full_filename, # Store the full path for matching
|
||||||
soulseek_client=self.soulseek_client
|
soulseek_client=self.soulseek_client
|
||||||
)
|
)
|
||||||
|
|
@ -4259,8 +4457,37 @@ class DownloadsPage(QWidget):
|
||||||
print(f"Error cleaning up finished download thread: {e}")
|
print(f"Error cleaning up finished download thread: {e}")
|
||||||
|
|
||||||
def clear_completed_downloads(self):
|
def clear_completed_downloads(self):
|
||||||
"""Clear completed downloads from the queue"""
|
"""Clear completed downloads from both slskd backend and local queue"""
|
||||||
self.download_queue.clear_completed_downloads()
|
if not self.soulseek_client:
|
||||||
|
print("[ERROR] No soulseek client available for clearing downloads")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run async clear operation
|
||||||
|
import asyncio
|
||||||
|
try:
|
||||||
|
# Get the current event loop or create a new one
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
print("[DEBUG] 🗑️ Clearing all completed downloads from slskd backend...")
|
||||||
|
success = loop.run_until_complete(self.soulseek_client.clear_all_completed_downloads())
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("[DEBUG] ✅ Successfully cleared completed downloads from backend")
|
||||||
|
# Also clear from local UI
|
||||||
|
self.download_queue.clear_completed_downloads()
|
||||||
|
# Trigger immediate status update to refresh UI
|
||||||
|
self.update_download_status()
|
||||||
|
else:
|
||||||
|
print("[ERROR] ❌ Failed to clear completed downloads from backend")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Exception during clear completed downloads: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def update_download_status(self):
|
def update_download_status(self):
|
||||||
"""Poll slskd API for download status updates (QTimer callback) - FIXED VERSION"""
|
"""Poll slskd API for download status updates (QTimer callback) - FIXED VERSION"""
|
||||||
|
|
@ -4410,12 +4637,22 @@ class DownloadsPage(QWidget):
|
||||||
new_status = 'downloading'
|
new_status = 'downloading'
|
||||||
elif 'Completed' in state or 'Succeeded' in state:
|
elif 'Completed' in state or 'Succeeded' in state:
|
||||||
new_status = 'completed'
|
new_status = 'completed'
|
||||||
|
# Construct absolute file path for completed downloads
|
||||||
|
api_filename = matching_transfer.get('filename', '')
|
||||||
|
if api_filename and self.soulseek_client and self.soulseek_client.download_path:
|
||||||
|
from pathlib import Path
|
||||||
|
# Convert API filename to absolute path using configured download directory
|
||||||
|
absolute_file_path = str(Path(self.soulseek_client.download_path) / api_filename)
|
||||||
|
print(f"[DEBUG] Constructed absolute path: {absolute_file_path}")
|
||||||
|
else:
|
||||||
|
absolute_file_path = download_item.file_path
|
||||||
|
|
||||||
# Update the download item status and progress BEFORE moving
|
# Update the download item status and progress BEFORE moving
|
||||||
download_item.update_status(
|
download_item.update_status(
|
||||||
status=new_status,
|
status=new_status,
|
||||||
progress=100, # Force 100% for completed downloads
|
progress=100, # Force 100% for completed downloads
|
||||||
download_speed=int(avg_speed),
|
download_speed=int(avg_speed),
|
||||||
file_path=matching_transfer.get('filename', download_item.file_path)
|
file_path=absolute_file_path
|
||||||
)
|
)
|
||||||
# Move completed items to finished queue
|
# Move completed items to finished queue
|
||||||
print(f"[DEBUG] Moving completed download '{download_item.title}' to finished queue")
|
print(f"[DEBUG] Moving completed download '{download_item.title}' to finished queue")
|
||||||
|
|
@ -4471,7 +4708,18 @@ class DownloadsPage(QWidget):
|
||||||
print(f"[DEBUG] Updated download ID for '{download_item.title}': {download_item.download_id} -> {transfer_id}")
|
print(f"[DEBUG] Updated download ID for '{download_item.title}': {download_item.download_id} -> {transfer_id}")
|
||||||
download_item.download_id = transfer_id
|
download_item.download_id = transfer_id
|
||||||
|
|
||||||
# (Matching transfer not found - debug message already printed above)
|
# If no matching transfer found, the download might have been removed from slskd
|
||||||
|
else:
|
||||||
|
# Check if this download was in finished state and might have been removed
|
||||||
|
if download_item in self.download_queue.finished_queue.download_items:
|
||||||
|
print(f"[DEBUG] 🗑️ Download '{download_item.title}' not found in API - likely removed from slskd")
|
||||||
|
print(f"[DEBUG] Removing '{download_item.title}' from finished downloads UI")
|
||||||
|
# Remove from finished queue since it's no longer in slskd
|
||||||
|
self.download_queue.finished_queue.remove_download_item(download_item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# After processing all download items, check for any that weren't found in the API
|
||||||
|
# This handles the case where downloads were removed from slskd externally
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Error processing transfer status update: {e}")
|
print(f"[ERROR] Error processing transfer status update: {e}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue