working better
This commit is contained in:
parent
f76cbf8210
commit
6154ed9519
6 changed files with 2647 additions and 108 deletions
|
|
@ -1 +1 @@
|
||||||
{"access_token": "BQBe_MJ7AUBigKGLrL5cDrQcNOcfu9QBWJfHn1YlwGrK7MHkoUaG0q_W6LGKB1qLKTLfqfLvZxBkiU57KA-PfRNM_TLmNOVrje3qqBsy3ILWlO85wNVqK2QEFVEOPdXK9nVyq_UlE9Olo0HiCYRmDShhJwghY7qCAHliDqO8ag2D0f8hZtvf_cf4pDNEq9BRtnGmlOsIMEefpexFt-d86t89Y8qtRQXM_70TsMiQMQEW1XULWWp9ppWAGcmzTYuX", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752167413, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
{"access_token": "BQAt2G_W1YOi76MMD9eqwcCyhCjip3RwAvqpGqygiqam8Z8f7EUU6Ow-G30LzdA-ZkhhdkdFiTOTZr1e4gzNhKPA2UFRS182N-7-QzyWEtBoHKmJm0ha8yosWEhb3MtZU5Ei7kYZ9Vqz6oP1NfAWxxQ86VH7eXbNe_XaaDw1SGv59XCwHm-EulvGDweyBIegm4akftwQ_6m57lyvtz-r7hTZ-KCJc72webluIBRjwLxn59TJ1NFq3vOwoZLmpZo6", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752183412, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||||
Binary file not shown.
|
|
@ -441,30 +441,45 @@ class SoulseekClient:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try different endpoints for getting downloads
|
# FIXED: Skip the 404 endpoint and go straight to the working one
|
||||||
response = await self._make_request('GET', 'downloads')
|
response = await self._make_request('GET', 'transfers/downloads')
|
||||||
if not response:
|
|
||||||
# Fallback to the old endpoint
|
|
||||||
response = await self._make_request('GET', 'transfers/downloads')
|
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
downloads = []
|
downloads = []
|
||||||
for download_data in response:
|
|
||||||
status = DownloadStatus(
|
|
||||||
id=download_data.get('id', ''),
|
|
||||||
filename=download_data.get('filename', ''),
|
|
||||||
username=download_data.get('username', ''),
|
|
||||||
state=download_data.get('state', ''),
|
|
||||||
progress=download_data.get('percentComplete', 0.0),
|
|
||||||
size=download_data.get('size', 0),
|
|
||||||
transferred=download_data.get('bytesTransferred', 0),
|
|
||||||
speed=download_data.get('averageSpeed', 0),
|
|
||||||
time_remaining=download_data.get('timeRemaining')
|
|
||||||
)
|
|
||||||
downloads.append(status)
|
|
||||||
|
|
||||||
|
# FIXED: Parse the nested response structure correctly
|
||||||
|
# Response format: [{"username": "user", "directories": [{"files": [...]}]}]
|
||||||
|
for user_data in response:
|
||||||
|
username = user_data.get('username', '')
|
||||||
|
directories = user_data.get('directories', [])
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
files = directory.get('files', [])
|
||||||
|
|
||||||
|
for file_data in files:
|
||||||
|
# Parse progress from the state if available
|
||||||
|
progress = 0.0
|
||||||
|
if file_data.get('state', '').lower().startswith('completed'):
|
||||||
|
progress = 100.0
|
||||||
|
elif 'progress' in file_data:
|
||||||
|
progress = float(file_data.get('progress', 0.0))
|
||||||
|
|
||||||
|
status = DownloadStatus(
|
||||||
|
id=file_data.get('id', ''),
|
||||||
|
filename=file_data.get('filename', ''),
|
||||||
|
username=username,
|
||||||
|
state=file_data.get('state', ''),
|
||||||
|
progress=progress,
|
||||||
|
size=file_data.get('size', 0),
|
||||||
|
transferred=file_data.get('bytesTransferred', 0), # May not exist in API
|
||||||
|
speed=file_data.get('averageSpeed', 0), # May not exist in API
|
||||||
|
time_remaining=file_data.get('timeRemaining')
|
||||||
|
)
|
||||||
|
downloads.append(status)
|
||||||
|
|
||||||
|
logger.debug(f"Parsed {len(downloads)} downloads from API response")
|
||||||
return downloads
|
return downloads
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
1942
logs/app.log
1942
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -1,9 +1,10 @@
|
||||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QFrame, QPushButton, QProgressBar, QListWidget,
|
QFrame, QPushButton, QProgressBar, QListWidget,
|
||||||
QListWidgetItem, QComboBox, QLineEdit, QScrollArea, QMessageBox,
|
QListWidgetItem, QComboBox, QLineEdit, QScrollArea, QMessageBox,
|
||||||
QSplitter, QSizePolicy, QSpacerItem)
|
QSplitter, QSizePolicy, QSpacerItem, QTabWidget)
|
||||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||||
from PyQt6.QtGui import QFont
|
from PyQt6.QtGui import QFont
|
||||||
|
import functools # For fixing lambda memory leaks
|
||||||
|
|
||||||
class DownloadThread(QThread):
|
class DownloadThread(QThread):
|
||||||
download_completed = pyqtSignal(str, object) # Download ID or success message, download_item
|
download_completed = pyqtSignal(str, object) # Download ID or success message, download_item
|
||||||
|
|
@ -249,8 +250,167 @@ class SearchThread(QThread):
|
||||||
"""Stop the search gracefully"""
|
"""Stop the search gracefully"""
|
||||||
self._stop_requested = True
|
self._stop_requested = True
|
||||||
|
|
||||||
|
class TrackedStatusUpdateThread(QThread):
|
||||||
|
"""Tracked status update thread that can be properly stopped and cleaned up"""
|
||||||
|
status_updated = pyqtSignal(list)
|
||||||
|
|
||||||
|
def __init__(self, soulseek_client, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.soulseek_client = soulseek_client
|
||||||
|
self._stop_requested = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
loop = None
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Check if stop was requested before starting
|
||||||
|
if self._stop_requested:
|
||||||
|
return
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
downloads = loop.run_until_complete(self.soulseek_client.get_all_downloads())
|
||||||
|
|
||||||
|
# Only emit if not stopped
|
||||||
|
if not self._stop_requested:
|
||||||
|
self.status_updated.emit(downloads or [])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not self._stop_requested:
|
||||||
|
print(f"Error fetching download status: {e}")
|
||||||
|
self.status_updated.emit([])
|
||||||
|
finally:
|
||||||
|
# Ensure proper cleanup
|
||||||
|
if loop:
|
||||||
|
try:
|
||||||
|
# Close any remaining tasks
|
||||||
|
pending = asyncio.all_tasks(loop)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||||
|
|
||||||
|
loop.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up status update event loop: {e}")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the status update thread gracefully"""
|
||||||
|
self._stop_requested = True
|
||||||
|
|
||||||
|
class StreamingThread(QThread):
|
||||||
|
"""Thread for streaming audio files without saving them permanently"""
|
||||||
|
streaming_started = pyqtSignal(str, object) # Message, search_result
|
||||||
|
streaming_finished = pyqtSignal(str, object) # Message, search_result
|
||||||
|
streaming_failed = pyqtSignal(str, object) # Error message, search_result
|
||||||
|
|
||||||
|
def __init__(self, soulseek_client, search_result):
|
||||||
|
super().__init__()
|
||||||
|
self.soulseek_client = soulseek_client
|
||||||
|
self.search_result = search_result
|
||||||
|
self._stop_requested = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
loop = None
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
self.streaming_started.emit(f"Starting stream: {self.search_result.filename}", self.search_result)
|
||||||
|
|
||||||
|
# Create a fresh event loop for this thread
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
# Create dedicated streaming temp folder
|
||||||
|
from config.settings import config_manager
|
||||||
|
download_path = config_manager.get('soulseek.download_path', './downloads')
|
||||||
|
temp_streaming_dir = os.path.join(download_path, 'temp_streaming')
|
||||||
|
|
||||||
|
# Ensure temp streaming directory exists
|
||||||
|
os.makedirs(temp_streaming_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Use consistent filename (overwrites previous stream)
|
||||||
|
file_extension = os.path.splitext(self.search_result.filename)[1]
|
||||||
|
temp_filename = f"current_stream{file_extension}"
|
||||||
|
temp_path = os.path.join(temp_streaming_dir, temp_filename)
|
||||||
|
|
||||||
|
# Remove any existing temp stream file
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
try:
|
||||||
|
os.remove(temp_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not remove existing temp file: {e}")
|
||||||
|
|
||||||
|
# Start the download to temporary streaming location
|
||||||
|
download_result = loop.run_until_complete(self._do_stream_download())
|
||||||
|
|
||||||
|
if not self._stop_requested:
|
||||||
|
if download_result:
|
||||||
|
# Stream started successfully - file is being downloaded to temp location
|
||||||
|
self.streaming_started.emit(f"Streaming: {self.search_result.filename}", self.search_result)
|
||||||
|
|
||||||
|
# Wait for download to complete by polling the temp location
|
||||||
|
max_wait_time = 30 # Wait up to 30 seconds
|
||||||
|
poll_interval = 1 # Check every second
|
||||||
|
|
||||||
|
for wait_count in range(max_wait_time):
|
||||||
|
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
|
||||||
|
# File exists and has content - streaming ready
|
||||||
|
self.streaming_finished.emit(f"Stream ready: {self.search_result.filename}", self.search_result)
|
||||||
|
self.temp_file_path = temp_path
|
||||||
|
print(f"✓ Stream file ready for playback: {temp_path}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Still downloading, wait a bit more
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
else:
|
||||||
|
# Timed out waiting for file
|
||||||
|
self.streaming_failed.emit("Stream download timed out", self.search_result)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.streaming_failed.emit("Streaming failed to start", self.search_result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not self._stop_requested:
|
||||||
|
self.streaming_failed.emit(str(e), self.search_result)
|
||||||
|
finally:
|
||||||
|
# Ensure proper cleanup
|
||||||
|
if loop:
|
||||||
|
try:
|
||||||
|
# Close any remaining tasks
|
||||||
|
pending = asyncio.all_tasks(loop)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||||
|
|
||||||
|
loop.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up streaming event loop: {e}")
|
||||||
|
|
||||||
|
async def _do_stream_download(self):
|
||||||
|
"""Perform the streaming download using normal download mechanism"""
|
||||||
|
# Use the same download mechanism as regular downloads
|
||||||
|
# The temp location will be handled by the download path configuration
|
||||||
|
return await self.soulseek_client.download(
|
||||||
|
self.search_result.username,
|
||||||
|
self.search_result.filename,
|
||||||
|
self.search_result.size
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the streaming gracefully"""
|
||||||
|
self._stop_requested = True
|
||||||
|
|
||||||
class SearchResultItem(QFrame):
|
class SearchResultItem(QFrame):
|
||||||
download_requested = pyqtSignal(object) # SearchResult object
|
download_requested = pyqtSignal(object) # SearchResult object
|
||||||
|
stream_requested = pyqtSignal(object) # SearchResult object for streaming
|
||||||
expansion_requested = pyqtSignal(object) # Signal when this item wants to expand
|
expansion_requested = pyqtSignal(object) # Signal when this item wants to expand
|
||||||
|
|
||||||
def __init__(self, search_result, parent=None):
|
def __init__(self, search_result, parent=None):
|
||||||
|
|
@ -286,7 +446,6 @@ class SearchResultItem(QFrame):
|
||||||
stop:0 rgba(50, 50, 50, 0.95),
|
stop:0 rgba(50, 50, 50, 0.95),
|
||||||
stop:1 rgba(40, 40, 40, 0.98));
|
stop:1 rgba(40, 40, 40, 0.98));
|
||||||
border: 1px solid rgba(29, 185, 84, 0.7);
|
border: 1px solid rgba(29, 185, 84, 0.7);
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
@ -325,7 +484,38 @@ class SearchResultItem(QFrame):
|
||||||
# Create both compact and expanded content but show only one
|
# Create both compact and expanded content but show only one
|
||||||
self.create_persistent_content(primary_info)
|
self.create_persistent_content(primary_info)
|
||||||
|
|
||||||
# Right section: Always-visible download button
|
# Right section: Play and download buttons
|
||||||
|
buttons_layout = QHBoxLayout()
|
||||||
|
buttons_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Play button for streaming preview
|
||||||
|
self.play_btn = QPushButton("▶️")
|
||||||
|
self.play_btn.setFixedSize(36, 36)
|
||||||
|
self.play_btn.clicked.connect(self.request_stream)
|
||||||
|
self.play_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(255, 193, 7, 0.9),
|
||||||
|
stop:1 rgba(255, 152, 0, 0.9));
|
||||||
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(255, 213, 79, 1.0),
|
||||||
|
stop:1 rgba(255, 171, 64, 1.0));
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 rgba(255, 152, 0, 1.0),
|
||||||
|
stop:1 rgba(245, 124, 0, 1.0));
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Download button
|
||||||
self.download_btn = QPushButton("⬇️")
|
self.download_btn = QPushButton("⬇️")
|
||||||
self.download_btn.setFixedSize(36, 36)
|
self.download_btn.setFixedSize(36, 36)
|
||||||
self.download_btn.clicked.connect(self.request_download)
|
self.download_btn.clicked.connect(self.request_download)
|
||||||
|
|
@ -356,8 +546,11 @@ class SearchResultItem(QFrame):
|
||||||
left_section.addWidget(music_icon)
|
left_section.addWidget(music_icon)
|
||||||
left_section.addWidget(self.content_widget, 1)
|
left_section.addWidget(self.content_widget, 1)
|
||||||
|
|
||||||
|
buttons_layout.addWidget(self.play_btn)
|
||||||
|
buttons_layout.addWidget(self.download_btn)
|
||||||
|
|
||||||
layout.addLayout(left_section, 1)
|
layout.addLayout(left_section, 1)
|
||||||
layout.addWidget(self.download_btn)
|
layout.addLayout(buttons_layout)
|
||||||
|
|
||||||
def create_persistent_content(self, primary_info):
|
def create_persistent_content(self, primary_info):
|
||||||
"""Create both compact and expanded content with visibility control"""
|
"""Create both compact and expanded content with visibility control"""
|
||||||
|
|
@ -662,6 +855,33 @@ class SearchResultItem(QFrame):
|
||||||
self.download_btn.setEnabled(False)
|
self.download_btn.setEnabled(False)
|
||||||
self.download_requested.emit(self.search_result)
|
self.download_requested.emit(self.search_result)
|
||||||
|
|
||||||
|
def request_stream(self):
|
||||||
|
"""Request streaming of this audio file"""
|
||||||
|
# Check if file is a valid audio type
|
||||||
|
audio_extensions = ['.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav']
|
||||||
|
filename_lower = self.search_result.filename.lower()
|
||||||
|
|
||||||
|
is_audio = any(filename_lower.endswith(ext) for ext in audio_extensions)
|
||||||
|
|
||||||
|
if is_audio:
|
||||||
|
# Change button state to indicate streaming
|
||||||
|
original_text = self.play_btn.text()
|
||||||
|
self.play_btn.setText("⏸️") # Pause icon to indicate playing
|
||||||
|
self.play_btn.setEnabled(False)
|
||||||
|
|
||||||
|
# Emit streaming request
|
||||||
|
self.stream_requested.emit(self.search_result)
|
||||||
|
|
||||||
|
# Reset button after a delay (basic state management)
|
||||||
|
QTimer.singleShot(2000, lambda: self.reset_play_state(original_text))
|
||||||
|
else:
|
||||||
|
print(f"Cannot stream non-audio file: {self.search_result.filename}")
|
||||||
|
|
||||||
|
def reset_play_state(self, original_text="▶️"):
|
||||||
|
"""Reset the play button state"""
|
||||||
|
self.play_btn.setText(original_text)
|
||||||
|
self.play_btn.setEnabled(True)
|
||||||
|
|
||||||
def reset_download_state(self):
|
def reset_download_state(self):
|
||||||
"""Reset the download button state"""
|
"""Reset the download button state"""
|
||||||
self.is_downloading = False
|
self.is_downloading = False
|
||||||
|
|
@ -670,7 +890,8 @@ class SearchResultItem(QFrame):
|
||||||
|
|
||||||
class DownloadItem(QFrame):
|
class DownloadItem(QFrame):
|
||||||
def __init__(self, title: str, artist: str, status: str, progress: int = 0,
|
def __init__(self, title: str, artist: str, status: str, progress: int = 0,
|
||||||
file_size: int = 0, download_speed: int = 0, file_path: str = "", parent=None):
|
file_size: int = 0, download_speed: int = 0, file_path: str = "",
|
||||||
|
download_id: str = "", soulseek_client=None, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artist = artist
|
self.artist = artist
|
||||||
|
|
@ -679,6 +900,8 @@ class DownloadItem(QFrame):
|
||||||
self.file_size = file_size
|
self.file_size = file_size
|
||||||
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 # Track download ID for cancellation
|
||||||
|
self.soulseek_client = soulseek_client # For cancellation functionality
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
|
|
@ -770,11 +993,11 @@ class DownloadItem(QFrame):
|
||||||
progress_layout = QVBoxLayout()
|
progress_layout = QVBoxLayout()
|
||||||
progress_layout.setSpacing(5)
|
progress_layout.setSpacing(5)
|
||||||
|
|
||||||
# Progress bar
|
# Progress bar - Store reference for safe updates
|
||||||
progress_bar = QProgressBar()
|
self.progress_bar = QProgressBar()
|
||||||
progress_bar.setFixedHeight(6)
|
self.progress_bar.setFixedHeight(6)
|
||||||
progress_bar.setValue(self.progress)
|
self.progress_bar.setValue(self.progress)
|
||||||
progress_bar.setStyleSheet("""
|
self.progress_bar.setStyleSheet("""
|
||||||
QProgressBar {
|
QProgressBar {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -786,29 +1009,42 @@ class DownloadItem(QFrame):
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Status text
|
# Status text - Store reference for safe updates with clean mapping
|
||||||
status_text = f"{self.status.title()}"
|
status_mapping = {
|
||||||
if self.status == "downloading":
|
"completed, succeeded": "Finished",
|
||||||
|
"completed, cancelled": "Cancelled",
|
||||||
|
"completed": "Finished",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"failed": "Failed",
|
||||||
|
"queued": "Queued"
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_status = status_mapping.get(self.status.lower(), self.status.title())
|
||||||
|
status_text = clean_status
|
||||||
|
|
||||||
|
if self.status.lower() in ["downloading", "queued"]:
|
||||||
status_text += f" - {self.progress}%"
|
status_text += f" - {self.progress}%"
|
||||||
|
|
||||||
status_label = QLabel(status_text)
|
self.status_label = QLabel(status_text)
|
||||||
status_label.setFont(QFont("Arial", 9))
|
self.status_label.setFont(QFont("Arial", 9))
|
||||||
status_label.setStyleSheet("color: #b3b3b3;")
|
self.status_label.setStyleSheet("color: #b3b3b3;")
|
||||||
|
|
||||||
progress_layout.addWidget(progress_bar)
|
progress_layout.addWidget(self.progress_bar)
|
||||||
progress_layout.addWidget(status_label)
|
progress_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
# Action buttons section
|
# Action buttons section
|
||||||
actions_layout = QVBoxLayout()
|
actions_layout = QVBoxLayout()
|
||||||
actions_layout.setSpacing(4)
|
actions_layout.setSpacing(4)
|
||||||
|
|
||||||
# Primary action button
|
# Primary action button
|
||||||
action_btn = QPushButton()
|
self.action_btn = QPushButton()
|
||||||
action_btn.setFixedSize(80, 28)
|
self.action_btn.setFixedSize(80, 28)
|
||||||
|
|
||||||
if self.status == "downloading":
|
if self.status == "downloading":
|
||||||
action_btn.setText("Cancel")
|
self.action_btn.setText("Cancel")
|
||||||
action_btn.setStyleSheet("""
|
self.action_btn.clicked.connect(self.cancel_download)
|
||||||
|
self.action_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid #e22134;
|
border: 1px solid #e22134;
|
||||||
|
|
@ -823,8 +1059,9 @@ class DownloadItem(QFrame):
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
elif self.status == "failed":
|
elif self.status == "failed":
|
||||||
action_btn.setText("Retry")
|
self.action_btn.setText("Retry")
|
||||||
action_btn.setStyleSheet("""
|
self.action_btn.clicked.connect(self.retry_download)
|
||||||
|
self.action_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid #1db954;
|
border: 1px solid #1db954;
|
||||||
|
|
@ -839,8 +1076,9 @@ class DownloadItem(QFrame):
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
else:
|
else:
|
||||||
action_btn.setText("Details")
|
self.action_btn.setText("Details")
|
||||||
action_btn.setStyleSheet("""
|
self.action_btn.clicked.connect(self.show_details)
|
||||||
|
self.action_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid #b3b3b3;
|
border: 1px solid #b3b3b3;
|
||||||
|
|
@ -874,7 +1112,7 @@ class DownloadItem(QFrame):
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
actions_layout.addWidget(action_btn)
|
actions_layout.addWidget(self.action_btn)
|
||||||
if self.status == "completed" and self.file_path:
|
if self.status == "completed" and self.file_path:
|
||||||
actions_layout.addWidget(location_btn)
|
actions_layout.addWidget(location_btn)
|
||||||
|
|
||||||
|
|
@ -923,7 +1161,8 @@ class DownloadItem(QFrame):
|
||||||
print(f"Error opening download location: {e}")
|
print(f"Error opening download location: {e}")
|
||||||
|
|
||||||
def update_status(self, status: str, progress: int = None, download_speed: int = None, file_path: str = None):
|
def update_status(self, status: str, progress: int = None, download_speed: int = None, file_path: str = None):
|
||||||
"""Update download item status and refresh UI"""
|
"""SAFE UPDATE: Update download item status without UI destruction"""
|
||||||
|
# Update properties
|
||||||
self.status = status
|
self.status = status
|
||||||
if progress is not None:
|
if progress is not None:
|
||||||
self.progress = progress
|
self.progress = progress
|
||||||
|
|
@ -932,19 +1171,100 @@ class DownloadItem(QFrame):
|
||||||
if file_path:
|
if file_path:
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
|
|
||||||
# Refresh the UI by recreating it
|
# SAFE UI UPDATES: Update widgets directly instead of recreating
|
||||||
# Clear current layout
|
try:
|
||||||
while self.layout().count():
|
# Update progress bar safely
|
||||||
child = self.layout().takeAt(0)
|
if hasattr(self, 'progress_bar') and self.progress_bar:
|
||||||
if child.widget():
|
self.progress_bar.setValue(self.progress)
|
||||||
child.widget().deleteLater()
|
|
||||||
|
# Update status label safely
|
||||||
# Recreate UI with updated values
|
if hasattr(self, 'status_label') and self.status_label:
|
||||||
self.setup_ui()
|
# Clean up status text display
|
||||||
|
status_mapping = {
|
||||||
|
"completed, succeeded": "Finished",
|
||||||
|
"completed, cancelled": "Cancelled",
|
||||||
|
"completed": "Finished",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"downloading": "Downloading",
|
||||||
|
"failed": "Failed",
|
||||||
|
"queued": "Queued"
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_status = status_mapping.get(self.status.lower(), self.status.title())
|
||||||
|
status_text = clean_status
|
||||||
|
|
||||||
|
if self.status.lower() in ["downloading", "queued"]:
|
||||||
|
status_text += f" - {self.progress}%"
|
||||||
|
|
||||||
|
self.status_label.setText(status_text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating download item UI: {e}")
|
||||||
|
# Fallback: only recreate if safe update fails
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def cancel_download(self):
|
||||||
|
"""Cancel the download using the SoulseekClient"""
|
||||||
|
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()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Failed to cancel download: {self.title}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cancelling download {self.title}: {e}")
|
||||||
|
|
||||||
|
def retry_download(self):
|
||||||
|
"""Retry a failed download"""
|
||||||
|
# For now, just update status back to downloading
|
||||||
|
# In a full implementation, this would restart the download
|
||||||
|
self.update_status("downloading", progress=0)
|
||||||
|
print(f"Retry requested for: {self.title}")
|
||||||
|
|
||||||
|
def show_details(self):
|
||||||
|
"""Show download details"""
|
||||||
|
details = f"""
|
||||||
|
Download Details:
|
||||||
|
Title: {self.title}
|
||||||
|
Artist: {self.artist}
|
||||||
|
Status: {self.status}
|
||||||
|
Progress: {self.progress}%
|
||||||
|
File Size: {self.file_size // (1024*1024)}MB
|
||||||
|
Download ID: {self.download_id}
|
||||||
|
File Path: {self.file_path}
|
||||||
|
"""
|
||||||
|
print(details)
|
||||||
|
|
||||||
class DownloadQueue(QFrame):
|
class DownloadQueue(QFrame):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, title="Download Queue", parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.queue_title = title
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
|
|
@ -964,9 +1284,9 @@ class DownloadQueue(QFrame):
|
||||||
header_layout = QHBoxLayout()
|
header_layout = QHBoxLayout()
|
||||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
title_label = QLabel("Download Queue")
|
self.title_label = QLabel(self.queue_title)
|
||||||
title_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
self.title_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
||||||
title_label.setStyleSheet("""
|
self.title_label.setStyleSheet("""
|
||||||
color: rgba(255, 255, 255, 0.95);
|
color: rgba(255, 255, 255, 0.95);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -979,7 +1299,7 @@ class DownloadQueue(QFrame):
|
||||||
padding: 0;
|
padding: 0;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
header_layout.addWidget(title_label)
|
header_layout.addWidget(self.title_label)
|
||||||
header_layout.addStretch()
|
header_layout.addStretch()
|
||||||
header_layout.addWidget(queue_count)
|
header_layout.addWidget(queue_count)
|
||||||
|
|
||||||
|
|
@ -1026,14 +1346,15 @@ class DownloadQueue(QFrame):
|
||||||
layout.addWidget(queue_scroll)
|
layout.addWidget(queue_scroll)
|
||||||
|
|
||||||
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, file_path: str = ""):
|
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
||||||
|
file_path: str = "", download_id: 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 download item
|
# Create new download item
|
||||||
item = DownloadItem(title, artist, status, progress, file_size, download_speed, file_path)
|
item = DownloadItem(title, artist, status, progress, file_size, download_speed, file_path, download_id, soulseek_client)
|
||||||
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)
|
||||||
|
|
@ -1073,6 +1394,117 @@ class DownloadQueue(QFrame):
|
||||||
for item in items_to_remove:
|
for item in items_to_remove:
|
||||||
self.remove_download_item(item)
|
self.remove_download_item(item)
|
||||||
|
|
||||||
|
class TabbedDownloadManager(QTabWidget):
|
||||||
|
"""Tabbed interface for managing active and finished downloads"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup the tabbed interface with active and finished download queues"""
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QTabWidget::pane {
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #282828;
|
||||||
|
}
|
||||||
|
QTabWidget::tab-bar {
|
||||||
|
alignment: center;
|
||||||
|
}
|
||||||
|
QTabBar::tab {
|
||||||
|
background: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #606060;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-right: 1px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
QTabBar::tab:selected {
|
||||||
|
background: #1db954;
|
||||||
|
color: #000000;
|
||||||
|
border: 1px solid #1db954;
|
||||||
|
}
|
||||||
|
QTabBar::tab:hover:!selected {
|
||||||
|
background: #505050;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create two download queues with appropriate titles
|
||||||
|
self.active_queue = DownloadQueue("Active Downloads")
|
||||||
|
self.finished_queue = DownloadQueue("Finished Downloads")
|
||||||
|
|
||||||
|
# Update the finished queue count label
|
||||||
|
self.finished_queue.queue_count_label.setText("No finished downloads")
|
||||||
|
|
||||||
|
# Add tabs
|
||||||
|
self.addTab(self.active_queue, "Download Queue")
|
||||||
|
self.addTab(self.finished_queue, "Finished Downloads")
|
||||||
|
|
||||||
|
# Set initial tab counts
|
||||||
|
self.update_tab_counts()
|
||||||
|
|
||||||
|
def add_download_item(self, title: str, artist: str, status: str = "queued",
|
||||||
|
progress: int = 0, file_size: int = 0, download_speed: int = 0,
|
||||||
|
file_path: str = "", download_id: str = "", soulseek_client=None):
|
||||||
|
"""Add a new download item to the active queue"""
|
||||||
|
item = self.active_queue.add_download_item(
|
||||||
|
title, artist, status, progress, file_size, download_speed,
|
||||||
|
file_path, download_id, soulseek_client
|
||||||
|
)
|
||||||
|
self.update_tab_counts()
|
||||||
|
return item
|
||||||
|
|
||||||
|
def move_to_finished(self, download_item):
|
||||||
|
"""Move a download item from active to finished queue"""
|
||||||
|
if download_item in self.active_queue.download_items:
|
||||||
|
# Remove from active queue
|
||||||
|
self.active_queue.remove_download_item(download_item)
|
||||||
|
|
||||||
|
# Add to finished queue
|
||||||
|
finished_item = self.finished_queue.add_download_item(
|
||||||
|
title=download_item.title,
|
||||||
|
artist=download_item.artist,
|
||||||
|
status=download_item.status,
|
||||||
|
progress=download_item.progress,
|
||||||
|
file_size=download_item.file_size,
|
||||||
|
download_speed=download_item.download_speed,
|
||||||
|
file_path=download_item.file_path,
|
||||||
|
download_id=download_item.download_id,
|
||||||
|
soulseek_client=download_item.soulseek_client
|
||||||
|
)
|
||||||
|
|
||||||
|
self.update_tab_counts()
|
||||||
|
return finished_item
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_tab_counts(self):
|
||||||
|
"""Update tab labels with current counts"""
|
||||||
|
active_count = len(self.active_queue.download_items)
|
||||||
|
finished_count = len(self.finished_queue.download_items)
|
||||||
|
|
||||||
|
self.setTabText(0, f"Download Queue ({active_count})")
|
||||||
|
self.setTabText(1, f"Finished Downloads ({finished_count})")
|
||||||
|
|
||||||
|
# Also update the download manager stats if they exist
|
||||||
|
if hasattr(self.parent(), 'update_download_manager_stats'):
|
||||||
|
self.parent().update_download_manager_stats(active_count, finished_count)
|
||||||
|
|
||||||
|
def clear_completed_downloads(self):
|
||||||
|
"""Clear completed downloads from the finished queue"""
|
||||||
|
self.finished_queue.clear_completed_downloads()
|
||||||
|
self.update_tab_counts()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def download_items(self):
|
||||||
|
"""Return all download items from active queue for compatibility"""
|
||||||
|
return self.active_queue.download_items
|
||||||
|
|
||||||
class DownloadsPage(QWidget):
|
class DownloadsPage(QWidget):
|
||||||
def __init__(self, soulseek_client=None, parent=None):
|
def __init__(self, soulseek_client=None, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -1081,6 +1513,7 @@ class DownloadsPage(QWidget):
|
||||||
self.explore_thread = None # Track API exploration thread
|
self.explore_thread = None # Track API exploration thread
|
||||||
self.session_thread = None # Track session info thread
|
self.session_thread = None # Track session info thread
|
||||||
self.download_threads = [] # Track active download threads
|
self.download_threads = [] # Track active download threads
|
||||||
|
self.status_update_threads = [] # Track status update threads (CRITICAL FIX)
|
||||||
self.search_results = []
|
self.search_results = []
|
||||||
self.download_items = [] # Track download items for the queue
|
self.download_items = [] # Track download items for the queue
|
||||||
self.displayed_results = 0 # Track how many results are currently displayed
|
self.displayed_results = 0 # Track how many results are currently displayed
|
||||||
|
|
@ -1405,16 +1838,16 @@ class DownloadsPage(QWidget):
|
||||||
stats_layout.setContentsMargins(12, 10, 12, 10)
|
stats_layout.setContentsMargins(12, 10, 12, 10)
|
||||||
stats_layout.setSpacing(6)
|
stats_layout.setSpacing(6)
|
||||||
|
|
||||||
active_downloads = QLabel("• Active Downloads: 0")
|
self.active_downloads_label = QLabel("• Active Downloads: 0")
|
||||||
active_downloads.setFont(QFont("Arial", 10))
|
self.active_downloads_label.setFont(QFont("Arial", 10))
|
||||||
active_downloads.setStyleSheet("color: rgba(255, 255, 255, 0.8);")
|
self.active_downloads_label.setStyleSheet("color: rgba(255, 255, 255, 0.8);")
|
||||||
|
|
||||||
queue_length = QLabel("• Queue Length: 0")
|
self.finished_downloads_label = QLabel("• Finished Downloads: 0")
|
||||||
queue_length.setFont(QFont("Arial", 10))
|
self.finished_downloads_label.setFont(QFont("Arial", 10))
|
||||||
queue_length.setStyleSheet("color: rgba(255, 255, 255, 0.8);")
|
self.finished_downloads_label.setStyleSheet("color: rgba(255, 255, 255, 0.8);")
|
||||||
|
|
||||||
stats_layout.addWidget(active_downloads)
|
stats_layout.addWidget(self.active_downloads_label)
|
||||||
stats_layout.addWidget(queue_length)
|
stats_layout.addWidget(self.finished_downloads_label)
|
||||||
layout.addWidget(stats_frame)
|
layout.addWidget(stats_frame)
|
||||||
|
|
||||||
# Control buttons
|
# Control buttons
|
||||||
|
|
@ -1443,15 +1876,25 @@ class DownloadsPage(QWidget):
|
||||||
controls_layout.addWidget(clear_btn)
|
controls_layout.addWidget(clear_btn)
|
||||||
layout.addWidget(controls_frame)
|
layout.addWidget(controls_frame)
|
||||||
|
|
||||||
# Download Queue Section
|
# Download Queue Section - Now with tabs for active and finished downloads
|
||||||
self.download_queue = DownloadQueue()
|
self.download_queue = TabbedDownloadManager()
|
||||||
layout.addWidget(self.download_queue)
|
layout.addWidget(self.download_queue)
|
||||||
|
|
||||||
|
# Initialize stats display
|
||||||
|
self.update_download_manager_stats(0, 0)
|
||||||
|
|
||||||
# Add stretch to push everything to top
|
# Add stretch to push everything to top
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
|
|
||||||
|
def update_download_manager_stats(self, active_count, finished_count):
|
||||||
|
"""Update the download manager statistics display"""
|
||||||
|
if hasattr(self, 'active_downloads_label'):
|
||||||
|
self.active_downloads_label.setText(f"• Active Downloads: {active_count}")
|
||||||
|
if hasattr(self, 'finished_downloads_label'):
|
||||||
|
self.finished_downloads_label.setText(f"• Finished Downloads: {finished_count}")
|
||||||
|
|
||||||
def create_compact_status_bar(self):
|
def create_compact_status_bar(self):
|
||||||
"""Create a minimal status bar"""
|
"""Create a minimal status bar"""
|
||||||
status_bar = QFrame()
|
status_bar = QFrame()
|
||||||
|
|
@ -1706,6 +2149,7 @@ class DownloadsPage(QWidget):
|
||||||
for result in results_to_show:
|
for result in results_to_show:
|
||||||
result_item = SearchResultItem(result)
|
result_item = SearchResultItem(result)
|
||||||
result_item.download_requested.connect(self.start_download)
|
result_item.download_requested.connect(self.start_download)
|
||||||
|
result_item.stream_requested.connect(self.start_stream)
|
||||||
result_item.expansion_requested.connect(self.handle_expansion_request)
|
result_item.expansion_requested.connect(self.handle_expansion_request)
|
||||||
# Insert before the stretch
|
# Insert before the stretch
|
||||||
insert_position = self.search_results_layout.count() - 1
|
insert_position = self.search_results_layout.count() - 1
|
||||||
|
|
@ -1802,6 +2246,7 @@ class DownloadsPage(QWidget):
|
||||||
result = self.search_results[i]
|
result = self.search_results[i]
|
||||||
result_item = SearchResultItem(result)
|
result_item = SearchResultItem(result)
|
||||||
result_item.download_requested.connect(self.start_download)
|
result_item.download_requested.connect(self.start_download)
|
||||||
|
result_item.stream_requested.connect(self.start_stream)
|
||||||
result_item.expansion_requested.connect(self.handle_expansion_request)
|
result_item.expansion_requested.connect(self.handle_expansion_request)
|
||||||
# Insert before the stretch (which is always last)
|
# Insert before the stretch (which is always last)
|
||||||
insert_position = self.search_results_layout.count() - 1
|
insert_position = self.search_results_layout.count() - 1
|
||||||
|
|
@ -1868,13 +2313,19 @@ class DownloadsPage(QWidget):
|
||||||
title = filename
|
title = filename
|
||||||
artist = search_result.username
|
artist = search_result.username
|
||||||
|
|
||||||
|
# Generate a unique download ID for tracking and cancellation
|
||||||
|
import time
|
||||||
|
download_id = f"{search_result.username}_{search_result.filename}_{int(time.time())}"
|
||||||
|
|
||||||
# Add to download queue immediately as "downloading"
|
# Add to download queue immediately as "downloading"
|
||||||
download_item = self.download_queue.add_download_item(
|
download_item = self.download_queue.add_download_item(
|
||||||
title=title,
|
title=title,
|
||||||
artist=artist,
|
artist=artist,
|
||||||
status="downloading",
|
status="downloading",
|
||||||
progress=0,
|
progress=0,
|
||||||
file_size=search_result.size
|
file_size=search_result.size,
|
||||||
|
download_id=download_id,
|
||||||
|
soulseek_client=self.soulseek_client
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create and start download thread
|
# Create and start download thread
|
||||||
|
|
@ -1882,7 +2333,10 @@ class DownloadsPage(QWidget):
|
||||||
download_thread.download_completed.connect(self.on_download_completed, Qt.ConnectionType.QueuedConnection)
|
download_thread.download_completed.connect(self.on_download_completed, Qt.ConnectionType.QueuedConnection)
|
||||||
download_thread.download_failed.connect(self.on_download_failed, Qt.ConnectionType.QueuedConnection)
|
download_thread.download_failed.connect(self.on_download_failed, Qt.ConnectionType.QueuedConnection)
|
||||||
download_thread.download_progress.connect(self.on_download_progress, Qt.ConnectionType.QueuedConnection)
|
download_thread.download_progress.connect(self.on_download_progress, Qt.ConnectionType.QueuedConnection)
|
||||||
download_thread.finished.connect(lambda: self.on_download_thread_finished(download_thread), Qt.ConnectionType.QueuedConnection)
|
download_thread.finished.connect(
|
||||||
|
functools.partial(self.on_download_thread_finished, download_thread),
|
||||||
|
Qt.ConnectionType.QueuedConnection
|
||||||
|
)
|
||||||
|
|
||||||
# Track the thread
|
# Track the thread
|
||||||
self.download_threads.append(download_thread)
|
self.download_threads.append(download_thread)
|
||||||
|
|
@ -1895,6 +2349,85 @@ class DownloadsPage(QWidget):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to start download: {str(e)}")
|
print(f"Failed to start download: {str(e)}")
|
||||||
|
|
||||||
|
def start_stream(self, search_result):
|
||||||
|
"""Start streaming a search result using StreamingThread"""
|
||||||
|
try:
|
||||||
|
print(f"Starting stream: {search_result.filename} from {search_result.username}")
|
||||||
|
|
||||||
|
# Check if file is a valid audio type
|
||||||
|
audio_extensions = ['.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav']
|
||||||
|
filename_lower = search_result.filename.lower()
|
||||||
|
|
||||||
|
is_audio = any(filename_lower.endswith(ext) for ext in audio_extensions)
|
||||||
|
|
||||||
|
if is_audio:
|
||||||
|
print(f"✓ Streaming audio file: {search_result.filename}")
|
||||||
|
print(f" Quality: {search_result.quality}")
|
||||||
|
print(f" Size: {search_result.size // (1024*1024)}MB")
|
||||||
|
print(f" User: {search_result.username}")
|
||||||
|
|
||||||
|
# Create and start streaming thread
|
||||||
|
streaming_thread = StreamingThread(self.soulseek_client, search_result)
|
||||||
|
streaming_thread.streaming_started.connect(self.on_streaming_started, Qt.ConnectionType.QueuedConnection)
|
||||||
|
streaming_thread.streaming_finished.connect(self.on_streaming_finished, Qt.ConnectionType.QueuedConnection)
|
||||||
|
streaming_thread.streaming_failed.connect(self.on_streaming_failed, Qt.ConnectionType.QueuedConnection)
|
||||||
|
streaming_thread.finished.connect(
|
||||||
|
functools.partial(self.on_streaming_thread_finished, streaming_thread),
|
||||||
|
Qt.ConnectionType.QueuedConnection
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track the streaming thread
|
||||||
|
if not hasattr(self, 'streaming_threads'):
|
||||||
|
self.streaming_threads = []
|
||||||
|
self.streaming_threads.append(streaming_thread)
|
||||||
|
|
||||||
|
# Start the streaming
|
||||||
|
streaming_thread.start()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"✗ Cannot stream non-audio file: {search_result.filename}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to start stream: {str(e)}")
|
||||||
|
|
||||||
|
def on_streaming_started(self, message, search_result):
|
||||||
|
"""Handle streaming start"""
|
||||||
|
print(f"Streaming started: {message}")
|
||||||
|
|
||||||
|
def on_streaming_finished(self, message, search_result):
|
||||||
|
"""Handle streaming completion"""
|
||||||
|
print(f"Streaming finished: {message}")
|
||||||
|
|
||||||
|
def on_streaming_failed(self, error_msg, search_result):
|
||||||
|
"""Handle streaming failure"""
|
||||||
|
print(f"Streaming failed: {error_msg}")
|
||||||
|
|
||||||
|
def on_streaming_thread_finished(self, thread):
|
||||||
|
"""Clean up when streaming thread finishes"""
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'streaming_threads') and thread in self.streaming_threads:
|
||||||
|
self.streaming_threads.remove(thread)
|
||||||
|
|
||||||
|
# Disconnect all signals to prevent stale connections
|
||||||
|
try:
|
||||||
|
thread.streaming_started.disconnect()
|
||||||
|
thread.streaming_finished.disconnect()
|
||||||
|
thread.streaming_failed.disconnect()
|
||||||
|
thread.finished.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass # Ignore if signals are already disconnected
|
||||||
|
|
||||||
|
# Ensure thread is properly stopped before deletion
|
||||||
|
if thread.isRunning():
|
||||||
|
thread.stop()
|
||||||
|
thread.wait(1000) # Wait up to 1 second
|
||||||
|
|
||||||
|
# Use QTimer.singleShot for delayed cleanup
|
||||||
|
QTimer.singleShot(100, thread.deleteLater)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up finished streaming thread: {e}")
|
||||||
|
|
||||||
def on_download_completed(self, message, download_item):
|
def on_download_completed(self, message, download_item):
|
||||||
"""Handle successful download start"""
|
"""Handle successful download start"""
|
||||||
print(f"Download success: {message}")
|
print(f"Download success: {message}")
|
||||||
|
|
@ -1949,43 +2482,41 @@ class DownloadsPage(QWidget):
|
||||||
self.download_queue.clear_completed_downloads()
|
self.download_queue.clear_completed_downloads()
|
||||||
|
|
||||||
def update_download_status(self):
|
def update_download_status(self):
|
||||||
"""Poll slskd API for download status updates (QTimer callback)"""
|
"""Poll slskd API for download status updates (QTimer callback) - FIXED VERSION"""
|
||||||
if not self.soulseek_client or not self.download_queue.download_items:
|
if not self.soulseek_client or not self.download_queue.download_items:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a thread to handle the async operation
|
# CRITICAL FIX: Use tracked thread instead of anonymous thread
|
||||||
from PyQt6.QtCore import QThread, pyqtSignal
|
|
||||||
|
|
||||||
class StatusUpdateThread(QThread):
|
|
||||||
status_updated = pyqtSignal(list)
|
|
||||||
|
|
||||||
def __init__(self, soulseek_client):
|
|
||||||
super().__init__()
|
|
||||||
self.soulseek_client = soulseek_client
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import asyncio
|
|
||||||
try:
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
downloads = loop.run_until_complete(self.soulseek_client.get_all_downloads())
|
|
||||||
self.status_updated.emit(downloads or [])
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error fetching download status: {e}")
|
|
||||||
self.status_updated.emit([])
|
|
||||||
finally:
|
|
||||||
if 'loop' in locals():
|
|
||||||
loop.close()
|
|
||||||
|
|
||||||
def handle_status_update(downloads):
|
def handle_status_update(downloads):
|
||||||
"""Handle the download status update in the main thread"""
|
"""Handle the download status update in the main thread - IMPROVED MATCHING"""
|
||||||
try:
|
try:
|
||||||
|
print(f"[DEBUG] Processing {len(downloads)} downloads from API")
|
||||||
|
|
||||||
for download_item in self.download_queue.download_items:
|
for download_item in self.download_queue.download_items:
|
||||||
# Find matching download by filename
|
# IMPROVED: Try multiple matching strategies
|
||||||
filename = f"{download_item.artist} - {download_item.title}"
|
queue_title = download_item.title.lower()
|
||||||
|
queue_artist = download_item.artist.lower()
|
||||||
|
|
||||||
|
print(f"[DEBUG] Looking for: '{queue_artist} - {queue_title}'")
|
||||||
|
|
||||||
|
found_match = False
|
||||||
for download in downloads:
|
for download in downloads:
|
||||||
if filename.lower() in download.filename.lower():
|
api_filename = download.filename.lower()
|
||||||
|
print(f"[DEBUG] Checking against: '{download.filename}'")
|
||||||
|
|
||||||
|
# Strategy 1: Check if title is in the filename
|
||||||
|
title_match = queue_title in api_filename
|
||||||
|
|
||||||
|
# Strategy 2: Check if artist is in the filename
|
||||||
|
artist_match = queue_artist in api_filename
|
||||||
|
|
||||||
|
# Strategy 3: Reverse check - see if any part of queue item is in API filename
|
||||||
|
combined_check = f"{queue_artist} - {queue_title}" in api_filename
|
||||||
|
|
||||||
|
if title_match or artist_match or combined_check:
|
||||||
|
print(f"[DEBUG] ✓ MATCH FOUND: {download.filename}")
|
||||||
|
print(f"[DEBUG] Status: {download.state}, Progress: {download.progress}%")
|
||||||
|
|
||||||
# Update the UI item with real data
|
# Update the UI item with real data
|
||||||
download_item.update_status(
|
download_item.update_status(
|
||||||
status=download.state,
|
status=download.state,
|
||||||
|
|
@ -1993,14 +2524,43 @@ class DownloadsPage(QWidget):
|
||||||
download_speed=download.speed,
|
download_speed=download.speed,
|
||||||
file_path=download.filename
|
file_path=download.filename
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# AUTO-MOVE: Check if download is finished and move to finished tab
|
||||||
|
finished_states = ["completed", "cancelled", "completed, succeeded", "completed, cancelled"]
|
||||||
|
if download.state.lower() in [state.lower() for state in finished_states]:
|
||||||
|
print(f"[DEBUG] Moving finished download to finished tab: {download_item.title}")
|
||||||
|
self.download_queue.move_to_finished(download_item)
|
||||||
|
|
||||||
|
found_match = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if not found_match:
|
||||||
|
print(f"[DEBUG] ✗ NO MATCH for: '{queue_artist} - {queue_title}'")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating download UI: {e}")
|
print(f"Error updating download UI: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
# Start the status update thread
|
def on_status_thread_finished(thread):
|
||||||
status_thread = StatusUpdateThread(self.soulseek_client)
|
"""Clean up status thread when finished"""
|
||||||
status_thread.status_updated.connect(handle_status_update)
|
try:
|
||||||
status_thread.finished.connect(status_thread.deleteLater)
|
if thread in self.status_update_threads:
|
||||||
|
self.status_update_threads.remove(thread)
|
||||||
|
thread.deleteLater()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up status thread: {e}")
|
||||||
|
|
||||||
|
# CRITICAL FIX: Create tracked status update thread
|
||||||
|
status_thread = TrackedStatusUpdateThread(self.soulseek_client, self)
|
||||||
|
status_thread.status_updated.connect(handle_status_update, Qt.ConnectionType.QueuedConnection)
|
||||||
|
status_thread.finished.connect(
|
||||||
|
functools.partial(on_status_thread_finished, status_thread),
|
||||||
|
Qt.ConnectionType.QueuedConnection
|
||||||
|
)
|
||||||
|
|
||||||
|
# CRITICAL FIX: Track the thread for proper cleanup
|
||||||
|
self.status_update_threads.append(status_thread)
|
||||||
status_thread.start()
|
status_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2041,6 +2601,28 @@ class DownloadsPage(QWidget):
|
||||||
self.session_thread.deleteLater()
|
self.session_thread.deleteLater()
|
||||||
self.session_thread = None
|
self.session_thread = None
|
||||||
|
|
||||||
|
# CRITICAL FIX: Stop all status update threads
|
||||||
|
for status_thread in self.status_update_threads[:]: # Copy list to avoid modification during iteration
|
||||||
|
try:
|
||||||
|
# Disconnect signals first
|
||||||
|
try:
|
||||||
|
status_thread.status_updated.disconnect()
|
||||||
|
status_thread.finished.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass # Ignore if signals are already disconnected
|
||||||
|
|
||||||
|
if status_thread.isRunning():
|
||||||
|
status_thread.stop()
|
||||||
|
status_thread.wait(2000) # Wait up to 2 seconds
|
||||||
|
if status_thread.isRunning():
|
||||||
|
status_thread.terminate()
|
||||||
|
status_thread.wait(1000)
|
||||||
|
status_thread.deleteLater()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up status update thread: {e}")
|
||||||
|
|
||||||
|
self.status_update_threads.clear()
|
||||||
|
|
||||||
# Stop all download threads with proper cleanup
|
# Stop all download threads with proper cleanup
|
||||||
for download_thread in self.download_threads[:]: # Copy list to avoid modification during iteration
|
for download_thread in self.download_threads[:]: # Copy list to avoid modification during iteration
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue