This commit is contained in:
Broque Thomas 2025-07-28 10:21:17 -07:00
parent ee66f1a5d8
commit d408727002
5 changed files with 2942 additions and 23 deletions

View file

@ -1 +1 @@
{"access_token": "BQBqPklgZl60MEbl4ylyWscvxWk8F2t6htU5zVOHsYsQiGkG1FWuCQxvuPVJlIA3w7EABwPb0fN0b_suYj5zErMzXx5odT3VsJ-q4y8cfa03ADDRxcr1AQgCt-QK1KGJqfcWSdACmV8L5m9Lu6W1JzKOVSW72XcWaMUkHLtLB4Qw6n88_idLANtU7BpsJOz4g2iJ7HNfo1HRaYNBrXLRFTl4nZ4q7AK1nvdlpZdDf2wLYatiTe4DbVDARFbGv66Y", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753689430, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
{"access_token": "BQDE-nRqhnnvzOYRj8mWSWJ8E1V7QYsSBRnVIS-Q5M0hGvuT0y9-VHhIxY2I8jbZeDitk3H9o3pzb39fc6v7K3oGMXatasuDIKYeeAntzAJxHjTQDg0fXh7HU1pbj3VdpBWa3gO7v5YMYKES9yMpJZ1tihruRhWEGRRoDZWLqyY3o7AyHjOYKOeMSrB1Cg-WPWKndRsnhyILJsVRVNIT8VST-dUHXCOLOB725sfw--lL2DmTCL3mG0rQHmHqPShP", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753726722, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
import sys
import asyncio
import time
from pathlib import Path
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QStackedWidget
from PyQt6.QtCore import QThread, pyqtSignal, QTimer
@ -61,6 +62,10 @@ class ServiceStatusThread(QThread):
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Track application start time for uptime calculation
self.app_start_time = time.time()
self.spotify_client = SpotifyClient()
self.plex_client = PlexClient()
self.soulseek_client = SoulseekClient()
@ -159,6 +164,7 @@ class MainWindow(QMainWindow):
# Configure dashboard with service clients and page references
self.dashboard_page.set_service_clients(self.spotify_client, self.plex_client, self.soulseek_client)
self.dashboard_page.set_page_references(self.downloads_page, self.sync_page)
self.dashboard_page.set_app_start_time(self.app_start_time)
self.stacked_widget.addWidget(self.dashboard_page)
self.stacked_widget.addWidget(self.sync_page)

View file

@ -6,6 +6,12 @@ from PyQt6.QtGui import QFont, QPalette, QColor
import time
import asyncio
import threading
try:
import resource
HAS_RESOURCE = True
except ImportError:
HAS_RESOURCE = False
import os
from typing import Optional, Dict, Any
from datetime import datetime
from dataclasses import dataclass
@ -39,12 +45,14 @@ class DashboardDataProvider(QObject):
download_stats_updated = pyqtSignal(int, int, float) # active, finished, speed
metadata_progress_updated = pyqtSignal(bool, str, int, int, float) # running, artist, processed, total, percentage
sync_progress_updated = pyqtSignal(str, int) # current_playlist, progress
system_stats_updated = pyqtSignal(str, str) # uptime, memory
def __init__(self, parent=None):
super().__init__(parent)
self.service_clients = {}
self.downloads_page = None
self.sync_page = None
self.app_start_time = None
# Data storage
self.service_status = {
@ -55,10 +63,14 @@ class DashboardDataProvider(QObject):
self.download_stats = DownloadStats()
self.metadata_progress = MetadataProgress()
# Update timers
# Update timers with different frequencies
self.download_stats_timer = QTimer()
self.download_stats_timer.timeout.connect(self.update_download_stats)
self.download_stats_timer.start(2000) # Update every 2 seconds
self.system_stats_timer = QTimer()
self.system_stats_timer.timeout.connect(self.update_system_stats)
self.system_stats_timer.start(10000) # Update every 10 seconds
def set_service_clients(self, spotify_client, plex_client, soulseek_client):
self.service_clients = {
@ -71,6 +83,9 @@ class DashboardDataProvider(QObject):
self.downloads_page = downloads_page
self.sync_page = sync_page
def set_app_start_time(self, start_time):
self.app_start_time = start_time
def update_service_status(self, service: str, connected: bool, response_time: float = 0.0, error: str = ""):
if service in self.service_status:
self.service_status[service].connected = connected
@ -85,11 +100,12 @@ class DashboardDataProvider(QObject):
active_count = len(self.downloads_page.download_queue.active_queue.download_items)
finished_count = len(self.downloads_page.download_queue.finished_queue.download_items)
# Calculate total speed from active downloads
# Calculate total speed from active downloads (in bytes/sec)
total_speed = 0.0
for item in self.downloads_page.download_queue.active_queue.download_items:
if hasattr(item, 'download_speed') and item.download_speed:
total_speed += item.download_speed
if hasattr(item, 'download_speed') and isinstance(item.download_speed, (int, float)) and item.download_speed > 0:
# download_speed is already in bytes/sec from slskd API
total_speed += float(item.download_speed)
self.download_stats.active_count = active_count
self.download_stats.finished_count = finished_count
@ -107,6 +123,78 @@ class DashboardDataProvider(QObject):
except Exception as e:
pass # Silent failure for stats updates
def update_system_stats(self):
"""Update system statistics (uptime and memory)"""
try:
uptime_str = self.get_uptime_string()
memory_str = self.get_memory_usage()
self.system_stats_updated.emit(uptime_str, memory_str)
except Exception as e:
pass
def get_uptime_string(self):
"""Get formatted uptime string"""
if not self.app_start_time:
return "Unknown"
try:
uptime_seconds = time.time() - self.app_start_time
if uptime_seconds < 60:
return f"{int(uptime_seconds)}s"
elif uptime_seconds < 3600:
minutes = int(uptime_seconds / 60)
return f"{minutes}m"
elif uptime_seconds < 86400:
hours = int(uptime_seconds / 3600)
minutes = int((uptime_seconds % 3600) / 60)
return f"{hours}h {minutes}m"
else:
days = int(uptime_seconds / 86400)
hours = int((uptime_seconds % 86400) / 3600)
return f"{days}d {hours}h"
except Exception:
return "Unknown"
def get_memory_usage(self):
"""Get formatted memory usage string"""
try:
# Try using resource module first (Unix-like systems)
if HAS_RESOURCE and hasattr(resource, 'RUSAGE_SELF'):
usage = resource.getrusage(resource.RUSAGE_SELF)
# ru_maxrss is in KB on Linux, bytes on macOS
max_rss = usage.ru_maxrss
# Detect platform and convert accordingly
import platform
if platform.system() == 'Darwin': # macOS
memory_mb = max_rss / (1024 * 1024)
else: # Linux
memory_mb = max_rss / 1024
return f"~{memory_mb:.0f} MB"
# Windows fallback: try psutil if available
try:
import psutil
process = psutil.Process(os.getpid())
memory_mb = process.memory_info().rss / (1024 * 1024)
return f"~{memory_mb:.0f} MB"
except ImportError:
pass
# Linux fallback: try reading /proc/self/status
if os.path.exists('/proc/self/status'):
with open('/proc/self/status', 'r') as f:
for line in f:
if line.startswith('VmRSS:'):
kb = int(line.split()[1])
return f"~{kb / 1024:.0f} MB"
return "N/A"
except Exception:
return "N/A"
def test_service_connection(self, service: str):
"""Test connection to a specific service"""
print(f"DEBUG: Testing {service} connection")
@ -149,7 +237,6 @@ class DashboardDataProvider(QObject):
del self._test_threads[service]
def on_service_test_completed(self, service: str, connected: bool, response_time: float, error: str):
print(f"DEBUG: on_service_test_completed called: {service}, {connected}, {response_time}, {error}")
self.update_service_status(service, connected, response_time, error)
class ServiceTestThread(QThread):
@ -161,37 +248,28 @@ class ServiceTestThread(QThread):
self.client = client
def run(self):
print(f"DEBUG: ServiceTestThread.run() started for {self.service}")
start_time = time.time()
connected = False
error = ""
try:
if self.service == 'spotify':
print(f"DEBUG: Testing Spotify authentication...")
connected = self.client.is_authenticated()
print(f"DEBUG: Spotify result: {connected}")
elif self.service == 'plex':
print(f"DEBUG: Testing Plex connection...")
connected = self.client.is_connected()
print(f"DEBUG: Plex result: {connected}")
elif self.service == 'soulseek':
print(f"DEBUG: Testing Soulseek connection...")
# Run async method in new event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
connected = loop.run_until_complete(self.client.check_connection())
print(f"DEBUG: Soulseek result: {connected}")
finally:
loop.close()
except Exception as e:
print(f"DEBUG: Exception in {self.service} test: {e}")
error = str(e)
connected = False
response_time = (time.time() - start_time) * 1000 # Convert to milliseconds
print(f"DEBUG: Emitting test_completed for {self.service}: connected={connected}, time={response_time:.0f}ms, error='{error}'")
self.test_completed.emit(self.service, connected, response_time, error)
# Ensure thread finishes properly
@ -535,6 +613,7 @@ class DashboardPage(QWidget):
self.data_provider.download_stats_updated.connect(self.on_download_stats_updated)
self.data_provider.metadata_progress_updated.connect(self.on_metadata_progress_updated)
self.data_provider.sync_progress_updated.connect(self.on_sync_progress_updated)
self.data_provider.system_stats_updated.connect(self.on_system_stats_updated)
# Service status cards
self.service_cards = {}
@ -552,6 +631,10 @@ class DashboardPage(QWidget):
"""Called from main window to provide page references for live data"""
self.data_provider.set_page_references(downloads_page, sync_page)
def set_app_start_time(self, start_time):
"""Called from main window to provide app start time for uptime calculation"""
self.data_provider.set_app_start_time(start_time)
def setup_ui(self):
self.setStyleSheet("""
DashboardPage {
@ -760,17 +843,14 @@ class DashboardPage(QWidget):
def test_service_connection(self, service: str):
"""Test connection to a specific service"""
print(f"DEBUG: Dashboard test_service_connection called for {service}")
if service in self.service_cards:
card = self.service_cards[service]
# Prevent multiple simultaneous tests
if hasattr(self.data_provider, '_test_threads') and service in self.data_provider._test_threads:
if self.data_provider._test_threads[service].isRunning():
print(f"DEBUG: Test already running for {service}")
return
print(f"DEBUG: Updating UI for {service} test")
card.test_button.setText("Testing...")
card.test_button.setEnabled(False)
@ -779,7 +859,6 @@ class DashboardPage(QWidget):
card.status_text.setText("Testing connection...")
# Start test
print(f"DEBUG: Calling data_provider.test_service_connection for {service}")
self.data_provider.test_service_connection(service)
def toggle_metadata_update(self):
@ -817,9 +896,12 @@ class DashboardPage(QWidget):
self.stats_cards['finished_downloads'].update_values(str(finished_count), "Completed today")
if 'download_speed' in self.stats_cards:
if total_speed > 1024 * 1024: # MB/s
# Format speed based on magnitude
if total_speed <= 0:
speed_text = "0 B/s"
elif total_speed >= 1024 * 1024: # MB/s
speed_text = f"{total_speed / (1024 * 1024):.1f} MB/s"
elif total_speed > 1024: # KB/s
elif total_speed >= 1024: # KB/s
speed_text = f"{total_speed / 1024:.1f} KB/s"
else:
speed_text = f"{total_speed:.0f} B/s"
@ -834,6 +916,14 @@ class DashboardPage(QWidget):
if 'active_syncs' in self.stats_cards:
self.stats_cards['active_syncs'].update_values(str(active_syncs), "Playlists syncing")
def on_system_stats_updated(self, uptime: str, memory: str):
"""Handle system statistics updates"""
if 'uptime' in self.stats_cards:
self.stats_cards['uptime'].update_values(uptime, "Application runtime")
if 'memory' in self.stats_cards:
self.stats_cards['memory'].update_values(memory, "Current usage")
def on_stat_card_clicked(self, card_title: str):
"""Handle stat card clicks for detailed views"""
# This can be implemented later for detailed views
@ -879,6 +969,8 @@ class DashboardPage(QWidget):
thread.deleteLater()
self.data_provider._test_threads.clear()
# Stop the data provider timer
# Stop the data provider timers
if hasattr(self.data_provider, 'download_stats_timer'):
self.data_provider.download_stats_timer.stop()
self.data_provider.download_stats_timer.stop()
if hasattr(self.data_provider, 'system_stats_timer'):
self.data_provider.system_stats_timer.stop()