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

View file

@ -6,6 +6,12 @@ from PyQt6.QtGui import QFont, QPalette, QColor
import time import time
import asyncio import asyncio
import threading import threading
try:
import resource
HAS_RESOURCE = True
except ImportError:
HAS_RESOURCE = False
import os
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from datetime import datetime from datetime import datetime
from dataclasses import dataclass from dataclasses import dataclass
@ -39,12 +45,14 @@ class DashboardDataProvider(QObject):
download_stats_updated = pyqtSignal(int, int, float) # active, finished, speed download_stats_updated = pyqtSignal(int, int, float) # active, finished, speed
metadata_progress_updated = pyqtSignal(bool, str, int, int, float) # running, artist, processed, total, percentage metadata_progress_updated = pyqtSignal(bool, str, int, int, float) # running, artist, processed, total, percentage
sync_progress_updated = pyqtSignal(str, int) # current_playlist, progress sync_progress_updated = pyqtSignal(str, int) # current_playlist, progress
system_stats_updated = pyqtSignal(str, str) # uptime, memory
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.service_clients = {} self.service_clients = {}
self.downloads_page = None self.downloads_page = None
self.sync_page = None self.sync_page = None
self.app_start_time = None
# Data storage # Data storage
self.service_status = { self.service_status = {
@ -55,10 +63,14 @@ class DashboardDataProvider(QObject):
self.download_stats = DownloadStats() self.download_stats = DownloadStats()
self.metadata_progress = MetadataProgress() self.metadata_progress = MetadataProgress()
# Update timers # Update timers with different frequencies
self.download_stats_timer = QTimer() self.download_stats_timer = QTimer()
self.download_stats_timer.timeout.connect(self.update_download_stats) self.download_stats_timer.timeout.connect(self.update_download_stats)
self.download_stats_timer.start(2000) # Update every 2 seconds 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): def set_service_clients(self, spotify_client, plex_client, soulseek_client):
self.service_clients = { self.service_clients = {
@ -71,6 +83,9 @@ class DashboardDataProvider(QObject):
self.downloads_page = downloads_page self.downloads_page = downloads_page
self.sync_page = sync_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 = ""): def update_service_status(self, service: str, connected: bool, response_time: float = 0.0, error: str = ""):
if service in self.service_status: if service in self.service_status:
self.service_status[service].connected = connected 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) active_count = len(self.downloads_page.download_queue.active_queue.download_items)
finished_count = len(self.downloads_page.download_queue.finished_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 total_speed = 0.0
for item in self.downloads_page.download_queue.active_queue.download_items: for item in self.downloads_page.download_queue.active_queue.download_items:
if hasattr(item, 'download_speed') and item.download_speed: if hasattr(item, 'download_speed') and isinstance(item.download_speed, (int, float)) and item.download_speed > 0:
total_speed += item.download_speed # 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.active_count = active_count
self.download_stats.finished_count = finished_count self.download_stats.finished_count = finished_count
@ -107,6 +123,78 @@ class DashboardDataProvider(QObject):
except Exception as e: except Exception as e:
pass # Silent failure for stats updates 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): def test_service_connection(self, service: str):
"""Test connection to a specific service""" """Test connection to a specific service"""
print(f"DEBUG: Testing {service} connection") print(f"DEBUG: Testing {service} connection")
@ -149,7 +237,6 @@ class DashboardDataProvider(QObject):
del self._test_threads[service] del self._test_threads[service]
def on_service_test_completed(self, service: str, connected: bool, response_time: float, error: str): 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) self.update_service_status(service, connected, response_time, error)
class ServiceTestThread(QThread): class ServiceTestThread(QThread):
@ -161,37 +248,28 @@ class ServiceTestThread(QThread):
self.client = client self.client = client
def run(self): def run(self):
print(f"DEBUG: ServiceTestThread.run() started for {self.service}")
start_time = time.time() start_time = time.time()
connected = False connected = False
error = "" error = ""
try: try:
if self.service == 'spotify': if self.service == 'spotify':
print(f"DEBUG: Testing Spotify authentication...")
connected = self.client.is_authenticated() connected = self.client.is_authenticated()
print(f"DEBUG: Spotify result: {connected}")
elif self.service == 'plex': elif self.service == 'plex':
print(f"DEBUG: Testing Plex connection...")
connected = self.client.is_connected() connected = self.client.is_connected()
print(f"DEBUG: Plex result: {connected}")
elif self.service == 'soulseek': elif self.service == 'soulseek':
print(f"DEBUG: Testing Soulseek connection...")
# Run async method in new event loop # Run async method in new event loop
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
try: try:
connected = loop.run_until_complete(self.client.check_connection()) connected = loop.run_until_complete(self.client.check_connection())
print(f"DEBUG: Soulseek result: {connected}")
finally: finally:
loop.close() loop.close()
except Exception as e: except Exception as e:
print(f"DEBUG: Exception in {self.service} test: {e}")
error = str(e) error = str(e)
connected = False connected = False
response_time = (time.time() - start_time) * 1000 # Convert to milliseconds 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) self.test_completed.emit(self.service, connected, response_time, error)
# Ensure thread finishes properly # 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.download_stats_updated.connect(self.on_download_stats_updated)
self.data_provider.metadata_progress_updated.connect(self.on_metadata_progress_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.sync_progress_updated.connect(self.on_sync_progress_updated)
self.data_provider.system_stats_updated.connect(self.on_system_stats_updated)
# Service status cards # Service status cards
self.service_cards = {} self.service_cards = {}
@ -552,6 +631,10 @@ class DashboardPage(QWidget):
"""Called from main window to provide page references for live data""" """Called from main window to provide page references for live data"""
self.data_provider.set_page_references(downloads_page, sync_page) 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): def setup_ui(self):
self.setStyleSheet(""" self.setStyleSheet("""
DashboardPage { DashboardPage {
@ -760,17 +843,14 @@ class DashboardPage(QWidget):
def test_service_connection(self, service: str): def test_service_connection(self, service: str):
"""Test connection to a specific service""" """Test connection to a specific service"""
print(f"DEBUG: Dashboard test_service_connection called for {service}")
if service in self.service_cards: if service in self.service_cards:
card = self.service_cards[service] card = self.service_cards[service]
# Prevent multiple simultaneous tests # Prevent multiple simultaneous tests
if hasattr(self.data_provider, '_test_threads') and service in self.data_provider._test_threads: if hasattr(self.data_provider, '_test_threads') and service in self.data_provider._test_threads:
if self.data_provider._test_threads[service].isRunning(): if self.data_provider._test_threads[service].isRunning():
print(f"DEBUG: Test already running for {service}")
return return
print(f"DEBUG: Updating UI for {service} test")
card.test_button.setText("Testing...") card.test_button.setText("Testing...")
card.test_button.setEnabled(False) card.test_button.setEnabled(False)
@ -779,7 +859,6 @@ class DashboardPage(QWidget):
card.status_text.setText("Testing connection...") card.status_text.setText("Testing connection...")
# Start test # Start test
print(f"DEBUG: Calling data_provider.test_service_connection for {service}")
self.data_provider.test_service_connection(service) self.data_provider.test_service_connection(service)
def toggle_metadata_update(self): def toggle_metadata_update(self):
@ -817,9 +896,12 @@ class DashboardPage(QWidget):
self.stats_cards['finished_downloads'].update_values(str(finished_count), "Completed today") self.stats_cards['finished_downloads'].update_values(str(finished_count), "Completed today")
if 'download_speed' in self.stats_cards: 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" 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" speed_text = f"{total_speed / 1024:.1f} KB/s"
else: else:
speed_text = f"{total_speed:.0f} B/s" speed_text = f"{total_speed:.0f} B/s"
@ -834,6 +916,14 @@ class DashboardPage(QWidget):
if 'active_syncs' in self.stats_cards: if 'active_syncs' in self.stats_cards:
self.stats_cards['active_syncs'].update_values(str(active_syncs), "Playlists syncing") 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): def on_stat_card_clicked(self, card_title: str):
"""Handle stat card clicks for detailed views""" """Handle stat card clicks for detailed views"""
# This can be implemented later for detailed views # This can be implemented later for detailed views
@ -879,6 +969,8 @@ class DashboardPage(QWidget):
thread.deleteLater() thread.deleteLater()
self.data_provider._test_threads.clear() self.data_provider._test_threads.clear()
# Stop the data provider timer # Stop the data provider timers
if hasattr(self.data_provider, 'download_stats_timer'): 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()