This commit is contained in:
Broque Thomas 2025-07-12 10:10:58 -07:00
parent 7bcac9466a
commit 7c7c54e617
7 changed files with 3616 additions and 6 deletions

View file

@ -1 +1 @@
{"access_token": "BQBWe7sCCLW9H6GTItLTJ6Fjw7YK6a_quPz3y2RgAXqTLwlQEMFLDO6xCmD8efMEe2OhV4B-MwzbcktuHT67wejexVVcnAJasEFH9RbVHUaVCP7MNgHuirirRazH6QweOd01DL3iieeN6lvGfRJ97BBkfQ-u5StX3foZZuxI4HOhCrALuIKJjWfROcjMRdy-qZeap_8nas0HwKOqowAxzqyjhRki0iZH128vrZe1sfCUM4895t-ULsMBWy2GAjTd", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752303544, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
{"access_token": "BQCzY4NDr26JqIBSxwATrH7MQMG5i9kCZxPvvtjcoVTyFZPRYPOEltaZEqN44ukQhNDo6C8fvkhlSPS6tk2N7zVitnBk9bT2IRAQiQGpxRS67kNlUfxUJSw52YAW83vECziyLfUfOkLs-E2LzoJDQtq_QOx18b_wO883mT1C8OyNFjVGvvjV5Srf8oSMJ4KN6WYwMpllfTbno7YlT7R5oPOtjgNquuUOhLHRWWp9KXEermO-MZWtWl_OxBAlhhmi", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752342994, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}

File diff suppressed because it is too large Load diff

View file

@ -137,6 +137,11 @@ class MainWindow(QMainWindow):
self.downloads_page.track_stopped.connect(self.sidebar.media_player.clear_track)
self.downloads_page.track_finished.connect(self.sidebar.media_player.clear_track)
# Connect loading animation signals
self.downloads_page.track_loading_started.connect(lambda result: self.sidebar.media_player.show_loading())
self.downloads_page.track_loading_finished.connect(lambda result: self.sidebar.media_player.hide_loading())
self.downloads_page.track_loading_progress.connect(lambda progress, result: self.sidebar.media_player.set_loading_progress(progress))
# Connect sidebar media player signals to downloads page
self.sidebar.media_player.play_pause_requested.connect(self.downloads_page.handle_sidebar_play_pause)
self.sidebar.media_player.stop_requested.connect(self.downloads_page.handle_sidebar_stop)

View file

@ -643,6 +643,7 @@ class StreamingThread(QThread):
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
streaming_progress = pyqtSignal(float, object) # Progress percentage (0-100), search_result
def __init__(self, soulseek_client, search_result):
super().__init__()
@ -697,12 +698,56 @@ class StreamingThread(QThread):
# Standard streaming - wait for complete download
max_wait_time = 45 # Wait up to 45 seconds
poll_interval = 2 # Check every 2 seconds
found_file = None
# Track elapsed time for fallback progress estimation
start_time = time.time()
estimated_duration = 15.0 # Estimate 15 seconds for typical download
last_progress_sent = 0.0
found_file = None # Initialize found_file outside the loop
for wait_count in range(max_wait_time // poll_interval):
if self._stop_requested:
break
# Calculate fallback progress based on elapsed time
elapsed_time = time.time() - start_time
estimated_progress = min(95.0, (elapsed_time / estimated_duration) * 95.0) # Cap at 95%
# Check download progress via slskd API
api_progress = None
try:
download_status = loop.run_until_complete(self._check_download_progress())
if download_status:
api_progress = download_status.progress
print(f"API Download progress: {api_progress:.1f}%")
# Check if download is complete
if download_status.state.lower().startswith('completed') or api_progress >= 100:
print(f"✓ Download completed via API status: {download_status.state}")
# Try to find the actual file - with retries for file system sync
for retry_count in range(5): # Try up to 5 times with delays
found_file = self._find_downloaded_file(download_path)
if found_file:
print(f"✓ Found completed file after {retry_count} retries: {found_file}")
break
else:
print(f"⏳ File not found yet, waiting... (retry {retry_count + 1}/5)")
time.sleep(1) # Wait 1 second for file system to sync
if found_file:
break # Exit main polling loop
except Exception as e:
print(f"Warning: Could not check download progress: {e}")
# Use API progress if available, otherwise use estimated progress
current_progress = api_progress if api_progress is not None and api_progress > 0 else estimated_progress
# Only emit progress updates if there's a meaningful change (>5%)
if current_progress - last_progress_sent >= 5.0:
self.streaming_progress.emit(current_progress, self.search_result)
print(f"Progress update: {current_progress:.1f}% (API: {api_progress}, Estimated: {estimated_progress:.1f}%)")
last_progress_sent = current_progress
# Search for the downloaded file in the downloads directory
found_file = self._find_downloaded_file(download_path)
@ -721,7 +766,8 @@ class StreamingThread(QThread):
# Clean up empty directories left behind
self._cleanup_empty_directories(download_path, found_file)
# Signal that streaming is ready
# Signal that streaming is ready (100% progress)
self.streaming_progress.emit(100.0, self.search_result)
self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result)
self.temp_file_path = stream_path
print(f"✓ Stream file ready for playback: {stream_path}")
@ -737,7 +783,39 @@ class StreamingThread(QThread):
time.sleep(poll_interval)
else:
# Timed out waiting for file
print(f"❌ Polling loop completed, timeout reached. found_file = {found_file}")
self.streaming_failed.emit("Stream download timed out - file not found", self.search_result)
# Post-polling file processing
print(f"📋 After polling loop: found_file = {found_file}")
if found_file:
print(f"🎯 Processing found file: {found_file}")
# Move the file to Stream folder with original filename
original_filename = os.path.basename(found_file)
stream_path = os.path.join(stream_folder, original_filename)
try:
# Move file to Stream folder
print(f"📁 Moving file from {found_file} to {stream_path}")
shutil.move(found_file, stream_path)
print(f"✅ Successfully moved file to stream folder: {stream_path}")
# Clean up empty directories left behind
self._cleanup_empty_directories(download_path, found_file)
# Signal that streaming is ready (100% progress)
self.streaming_progress.emit(100.0, self.search_result)
self.streaming_finished.emit(f"Stream ready: {os.path.basename(found_file)}", self.search_result)
self.temp_file_path = stream_path
print(f"🎵 Stream file ready for playback: {stream_path}")
except Exception as e:
print(f"❌ Error moving file to stream folder: {e}")
self.streaming_failed.emit(f"Failed to prepare stream file: {e}", self.search_result)
else:
print(f"❌ No file found after polling loop completed")
self.streaming_failed.emit("Stream download failed - file not found after completion", self.search_result)
else:
self.streaming_failed.emit("Streaming failed to start", self.search_result)
@ -761,6 +839,33 @@ class StreamingThread(QThread):
except Exception as e:
print(f"Error cleaning up streaming event loop: {e}")
async def _check_download_progress(self):
"""Check download progress via slskd API for the current streaming download"""
try:
# Get all current downloads and find ours by filename match
all_downloads = await self.soulseek_client.get_all_downloads()
print(f"🔍 Looking for download - Target: {self.search_result.username}:{os.path.basename(self.search_result.filename)}")
print(f"🔍 Found {len(all_downloads)} total downloads in API")
# Look for our specific file by filename and username
target_filename = os.path.basename(self.search_result.filename)
target_username = self.search_result.username
for i, download in enumerate(all_downloads):
download_filename = os.path.basename(download.filename)
print(f"📁 Download {i+1}: {download.username}:{download_filename} - State: {download.state} - Progress: {download.progress}%")
if (download_filename == target_filename and
download.username == target_username):
print(f"✅ Found matching download: {download.state} - {download.progress}%")
return download
print(f"❌ No matching download found for {target_username}:{target_filename}")
return None
except Exception as e:
print(f"Error checking download progress: {e}")
return None
def _find_downloaded_file(self, download_path):
"""Find the downloaded audio file in the downloads directory tree"""
import os
@ -2896,6 +3001,9 @@ class DownloadsPage(QWidget):
track_stopped = pyqtSignal()
track_finished = pyqtSignal()
track_position_updated = pyqtSignal(float, float) # current_position, duration in seconds
track_loading_started = pyqtSignal(object) # Track result object when streaming starts
track_loading_finished = pyqtSignal(object) # Track result object when streaming completes
track_loading_progress = pyqtSignal(float, object) # Progress percentage (0-100), track result object
def __init__(self, soulseek_client=None, parent=None):
super().__init__(parent)
@ -4342,6 +4450,7 @@ class DownloadsPage(QWidget):
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_progress.connect(self.on_streaming_progress, 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),
@ -4372,6 +4481,9 @@ class DownloadsPage(QWidget):
except RuntimeError:
# Button was deleted, ignore
pass
# Emit signal for media player loading animation
self.track_loading_started.emit(search_result)
def on_streaming_finished(self, message, search_result):
"""Handle streaming completion - start actual audio playback"""
@ -4419,6 +4531,7 @@ class DownloadsPage(QWidget):
pass
# Emit track started signal for sidebar media player
if hasattr(self, 'current_track_result') and self.current_track_result:
self.track_loading_finished.emit(self.current_track_result)
self.track_started.emit(self.current_track_result)
else:
print(f"❌ Failed to start audio playback")
@ -4452,6 +4565,21 @@ class DownloadsPage(QWidget):
pass
self.currently_playing_button = None
def on_streaming_progress(self, progress_percent, search_result):
"""Handle streaming progress updates"""
print(f"Streaming progress: {progress_percent:.1f}% for {search_result.filename}")
# Check if this progress is for the currently requested track
if hasattr(self, 'current_track_result') and self.current_track_result:
current_track_id = f"{self.current_track_result.username}:{self.current_track_result.filename}"
progress_track_id = f"{search_result.username}:{search_result.filename}"
if current_track_id == progress_track_id:
# Emit progress signal for media player
self.track_loading_progress.emit(progress_percent, search_result)
else:
print(f"🚫 Ignoring progress for old streaming result: {search_result.filename}")
def on_streaming_failed(self, error_msg, search_result):
"""Handle streaming failure"""
print(f"Streaming failed: {error_msg}")

View file

@ -1,7 +1,7 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QFrame, QSizePolicy, QSpacerItem, QSlider, QProgressBar)
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QTimer
from PyQt6.QtGui import QFont, QPalette, QIcon, QPixmap, QPainter, QFontMetrics
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QTimer, pyqtProperty
from PyQt6.QtGui import QFont, QPalette, QIcon, QPixmap, QPainter, QFontMetrics, QColor, QLinearGradient
class ScrollingLabel(QLabel):
"""A label that smoothly scrolls text horizontally when it's too long to fit"""
@ -285,6 +285,173 @@ class StatusIndicator(QWidget):
""")
self.service_label.setStyleSheet("color: #b3b3b3; font-weight: 400;")
class LoadingAnimation(QWidget):
"""Thin horizontal loading animation for media player with dual-mode capability"""
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedHeight(12) # Increased height for text overlay
self._progress = 0.0
self._is_active = False
self._mode = "indefinite" # "indefinite" or "determinate"
self._determinate_progress = 0.0 # 0-100% for determinate mode
# Animation setup for indefinite mode
self.animation = QPropertyAnimation(self, b"progress")
self.animation.setDuration(1200) # 1.2 second cycle
self.animation.setStartValue(0.0)
self.animation.setEndValue(1.0)
self.animation.setLoopCount(-1) # Infinite loop
self.animation.setEasingCurve(QEasingCurve.Type.InOutSine)
# Progress value animation for smooth transitions in determinate mode
self.progress_animation = QPropertyAnimation(self, b"determinate_progress")
self.progress_animation.setDuration(300) # Smooth 300ms transitions
self.progress_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
# Completion glow effect
self._glow_opacity = 0.0
self.glow_animation = QPropertyAnimation(self, b"glow_opacity")
self.glow_animation.setDuration(800) # Slower glow pulse
self.glow_animation.setStartValue(0.0)
self.glow_animation.setEndValue(1.0)
self.glow_animation.setLoopCount(3) # Pulse 3 times
self.glow_animation.setEasingCurve(QEasingCurve.Type.InOutSine)
self.hide() # Start hidden
@pyqtProperty(float)
def progress(self):
return self._progress
@progress.setter
def progress(self, value):
self._progress = value
self.update()
@pyqtProperty(float)
def determinate_progress(self):
return self._determinate_progress
@determinate_progress.setter
def determinate_progress(self, value):
self._determinate_progress = value
self.update()
@pyqtProperty(float)
def glow_opacity(self):
return self._glow_opacity
@glow_opacity.setter
def glow_opacity(self, value):
self._glow_opacity = value
self.update()
def start_animation(self):
"""Start the indefinite loading animation"""
self._is_active = True
self._mode = "indefinite"
self.show()
self.animation.start()
def set_progress(self, percentage):
"""Set determinate progress (0-100%) with smooth animation"""
if not self._is_active:
self._is_active = True
self.show()
# Switch to determinate mode
if self._mode == "indefinite":
self._mode = "determinate"
self.animation.stop() # Stop indefinite animation
# Animate to new progress value
self.progress_animation.setStartValue(self._determinate_progress)
self.progress_animation.setEndValue(percentage)
self.progress_animation.start()
# Trigger completion glow effect when reaching 100%
if percentage >= 100 and self._determinate_progress < 100:
self.glow_animation.start()
def stop_animation(self):
"""Stop the loading animation"""
self._is_active = False
self._mode = "indefinite"
self.animation.stop()
self.progress_animation.stop()
self.glow_animation.stop()
self.hide()
self._progress = 0.0
self._determinate_progress = 0.0
self._glow_opacity = 0.0
self.update()
def paintEvent(self, event):
"""Custom paint event for dual-mode animation with text overlay"""
if not self._is_active:
return
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
width = self.width()
height = self.height()
progress_bar_height = 4 # Bottom 4px for progress bar
text_height = height - progress_bar_height # Top area for text
# Background for progress bar area
progress_rect = self.rect()
progress_rect.setTop(text_height)
painter.fillRect(progress_rect, QColor(40, 40, 40))
if self._mode == "indefinite":
# Indefinite mode: animated gradient wave
gradient_width = width * 0.3 # 30% of total width
center_x = self._progress * width
for i in range(int(gradient_width)):
alpha = max(0, 255 - (abs(i - gradient_width/2) * 8))
color = QColor(29, 185, 84, int(alpha)) # Spotify green with fade
x = int(center_x - gradient_width/2 + i)
if 0 <= x < width:
painter.fillRect(x, text_height, 1, progress_bar_height, color)
else: # determinate mode
# Determinate mode: progress bar with percentage
progress_width = (self._determinate_progress / 100.0) * width
# Progress bar with gradient
if progress_width > 0:
progress_fill_rect = QRect(0, text_height, int(progress_width), progress_bar_height)
# Create subtle gradient for progress bar
gradient = QLinearGradient(0, text_height, progress_width, text_height)
gradient.setColorAt(0, QColor(29, 185, 84)) # Spotify green
gradient.setColorAt(1, QColor(30, 215, 96)) # Lighter green
painter.fillRect(progress_fill_rect, gradient)
# Add animated glow effect during completion
if self._glow_opacity > 0:
glow_alpha = int(120 * self._glow_opacity) # Max alpha of 120
glow_color = QColor(29, 185, 84, glow_alpha)
# Expand glow slightly beyond progress bar for effect
glow_rect = QRect(0, text_height - 1, width, progress_bar_height + 2)
painter.fillRect(glow_rect, glow_color)
# Percentage text overlay (elegant, small font)
if text_height > 0 and self._determinate_progress > 0:
font = QFont("Segoe UI", 7, QFont.Weight.Medium) # Small, elegant font
painter.setFont(font)
painter.setPen(QColor(180, 180, 180)) # Light gray text
percentage_text = f"{int(self._determinate_progress)}%"
text_rect = QRect(0, 0, width, text_height)
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, percentage_text)
class MediaPlayer(QWidget):
# Signals for media control
play_pause_requested = pyqtSignal()
@ -321,6 +488,10 @@ class MediaPlayer(QWidget):
layout.setContentsMargins(16, 10, 16, 10)
layout.setSpacing(10)
# Loading animation at the top
self.loading_animation = LoadingAnimation()
layout.addWidget(self.loading_animation)
# Always visible header with basic controls
self.header = self.create_header()
layout.addWidget(self.header)
@ -581,6 +752,9 @@ class MediaPlayer(QWidget):
# Set to playing state (show pause button since track just started)
self.set_playing_state(True)
# Hide loading animation now that track is ready
self.hide_loading()
# Hide no track message and show player
self.no_track_label.setVisible(False)
@ -606,8 +780,9 @@ class MediaPlayer(QWidget):
self.current_track = None
self.is_playing = False
# Stop any scrolling animation
# Stop any animations
self.track_info.stop_scrolling()
self.hide_loading()
# Update UI
self.track_info.setText("No track")
@ -636,6 +811,18 @@ class MediaPlayer(QWidget):
volume = value / 100.0 # Convert to 0.0-1.0
self.volume_changed.emit(volume)
def show_loading(self):
"""Show and start the loading animation"""
self.loading_animation.start_animation()
def hide_loading(self):
"""Hide and stop the loading animation"""
self.loading_animation.stop_animation()
def set_loading_progress(self, percentage):
"""Set loading progress percentage (0-100)"""
self.loading_animation.set_progress(percentage)
class ModernSidebar(QWidget):
page_changed = pyqtSignal(str)