pull/2/head
Broque Thomas 9 months ago
parent fb5c5a791a
commit ee66f1a5d8

@ -1 +1 @@
{"access_token": "BQAdyqwBcWjHXyNglSbWavYUmdxzOiQMWw57Cw-vulDpJHzqwR7GrNabHbXNRoRi17XxQ3aov7_FUiIlsBlJ2eQpRbaDMMvEld4Xo99eVnQKh3S6S6-ialTKQi6GaATUEEro2C0OQNpWml6xPLGXMZ6Oe5m48Neldipw7uE2P_3RQXMqjQurbbZ9ujGcJv6_MNOMfWhF0eZ7ix3Rj8P-lasEebj1G7uZ9YdWBgmgclApiFWQa1Z_moB0Zuomb9Ah", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753682064, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
{"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"}

File diff suppressed because it is too large Load Diff

@ -156,6 +156,10 @@ class MainWindow(QMainWindow):
self.artists_page = ArtistsPage(downloads_page=self.downloads_page)
self.settings_page = SettingsPage()
# 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.stacked_widget.addWidget(self.dashboard_page)
self.stacked_widget.addWidget(self.sync_page)
self.stacked_widget.addWidget(self.downloads_page)
@ -255,6 +259,10 @@ class MainWindow(QMainWindow):
def update_service_status(self, service: str, connected: bool):
self.sidebar.update_service_status(service, connected)
# Update dashboard with service status
if hasattr(self.dashboard_page, 'data_provider'):
self.dashboard_page.data_provider.update_service_status(service, connected)
# Force a refresh of the Spotify client if needed
if service == "spotify" and not connected:
try:

@ -1,25 +1,222 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QFrame, QGridLayout, QScrollArea, QSizePolicy)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QFont, QPalette
QFrame, QGridLayout, QScrollArea, QSizePolicy, QPushButton,
QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject
from PyQt6.QtGui import QFont, QPalette, QColor
import time
import asyncio
import threading
from typing import Optional, Dict, Any
from datetime import datetime
from dataclasses import dataclass
@dataclass
class ServiceStatus:
name: str
connected: bool
last_check: datetime
response_time: float = 0.0
error: Optional[str] = None
@dataclass
class DownloadStats:
active_count: int = 0
finished_count: int = 0
total_speed: float = 0.0
total_transferred: int = 0
@dataclass
class MetadataProgress:
is_running: bool = False
current_artist: str = ""
processed_count: int = 0
total_count: int = 0
progress_percentage: float = 0.0
class DashboardDataProvider(QObject):
# Signals for real-time updates
service_status_updated = pyqtSignal(str, bool, float, str) # service, connected, response_time, error
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
def __init__(self, parent=None):
super().__init__(parent)
self.service_clients = {}
self.downloads_page = None
self.sync_page = None
# Data storage
self.service_status = {
'spotify': ServiceStatus('Spotify', False, datetime.now()),
'plex': ServiceStatus('Plex', False, datetime.now()),
'soulseek': ServiceStatus('Soulseek', False, datetime.now())
}
self.download_stats = DownloadStats()
self.metadata_progress = MetadataProgress()
# Update timers
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
def set_service_clients(self, spotify_client, plex_client, soulseek_client):
self.service_clients = {
'spotify': spotify_client,
'plex': plex_client,
'soulseek': soulseek_client
}
def set_page_references(self, downloads_page, sync_page):
self.downloads_page = downloads_page
self.sync_page = sync_page
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
self.service_status[service].last_check = datetime.now()
self.service_status[service].response_time = response_time
self.service_status[service].error = error
self.service_status_updated.emit(service, connected, response_time, error)
def update_download_stats(self):
if self.downloads_page and hasattr(self.downloads_page, 'download_queue'):
try:
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
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
self.download_stats.active_count = active_count
self.download_stats.finished_count = finished_count
self.download_stats.total_speed = total_speed
self.download_stats_updated.emit(active_count, finished_count, total_speed)
except Exception as e:
pass # Silent failure for stats updates
# Update sync stats
if self.sync_page and hasattr(self.sync_page, 'active_sync_workers'):
try:
active_syncs = len(self.sync_page.active_sync_workers)
self.sync_progress_updated.emit("", active_syncs)
except Exception as e:
pass # Silent failure for stats updates
def test_service_connection(self, service: str):
"""Test connection to a specific service"""
print(f"DEBUG: Testing {service} connection")
print(f"DEBUG: Available service clients: {list(self.service_clients.keys())}")
if service not in self.service_clients:
print(f"DEBUG: Service {service} not found in service_clients")
return
print(f"DEBUG: Service client for {service}: {self.service_clients[service]}")
# Clean up any existing test thread for this service
if hasattr(self, '_test_threads') and service in self._test_threads:
old_thread = self._test_threads[service]
if old_thread.isRunning():
old_thread.quit()
old_thread.wait()
old_thread.deleteLater()
# Initialize test threads dict if needed
if not hasattr(self, '_test_threads'):
self._test_threads = {}
# Run connection test in background thread
test_thread = ServiceTestThread(service, self.service_clients[service])
test_thread.test_completed.connect(self.on_service_test_completed)
test_thread.finished.connect(lambda: self._cleanup_test_thread(service))
self._test_threads[service] = test_thread
print(f"DEBUG: Starting test thread for {service}")
test_thread.start()
def _cleanup_test_thread(self, service: str):
"""Clean up completed test thread"""
if hasattr(self, '_test_threads') and service in self._test_threads:
thread = self._test_threads[service]
if thread.isRunning():
thread.quit()
thread.wait(1000) # Wait up to 1 second
thread.deleteLater()
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):
test_completed = pyqtSignal(str, bool, float, str) # service, connected, response_time, error
def __init__(self, service: str, client, parent=None):
super().__init__(parent)
self.service = service
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
self.quit()
class StatCard(QFrame):
def __init__(self, title: str, value: str, subtitle: str = "", parent=None):
def __init__(self, title: str, value: str, subtitle: str = "", clickable: bool = False, parent=None):
super().__init__(parent)
self.clickable = clickable
self.title_text = title
self.setup_ui(title, value, subtitle)
def setup_ui(self, title: str, value: str, subtitle: str):
self.setFixedHeight(120)
self.setStyleSheet("""
StatCard {
hover_style = "border: 1px solid #1db954;" if self.clickable else ""
self.setStyleSheet(f"""
StatCard {{
background: #282828;
border-radius: 8px;
border: 1px solid #404040;
}
StatCard:hover {
}}
StatCard:hover {{
background: #333333;
border: 1px solid #1db954;
}
{hover_style}
}}
""")
layout = QVBoxLayout(self)
@ -27,25 +224,255 @@ class StatCard(QFrame):
layout.setSpacing(5)
# Title
title_label = QLabel(title)
title_label.setFont(QFont("Arial", 10))
title_label.setStyleSheet("color: #b3b3b3;")
self.title_label = QLabel(title)
self.title_label.setFont(QFont("Arial", 10))
self.title_label.setStyleSheet("color: #b3b3b3;")
# Value
value_label = QLabel(value)
value_label.setFont(QFont("Arial", 24, QFont.Weight.Bold))
value_label.setStyleSheet("color: #ffffff;")
self.value_label = QLabel(value)
self.value_label.setFont(QFont("Arial", 24, QFont.Weight.Bold))
self.value_label.setStyleSheet("color: #ffffff;")
# Subtitle
self.subtitle_label = None
if subtitle:
subtitle_label = QLabel(subtitle)
subtitle_label.setFont(QFont("Arial", 9))
subtitle_label.setStyleSheet("color: #b3b3b3;")
layout.addWidget(subtitle_label)
self.subtitle_label = QLabel(subtitle)
self.subtitle_label.setFont(QFont("Arial", 9))
self.subtitle_label.setStyleSheet("color: #b3b3b3;")
layout.addWidget(self.subtitle_label)
layout.addWidget(self.title_label)
layout.addWidget(self.value_label)
layout.addStretch()
def update_values(self, value: str, subtitle: str = ""):
self.value_label.setText(value)
if self.subtitle_label and subtitle:
self.subtitle_label.setText(subtitle)
def mousePressEvent(self, event):
if self.clickable:
self.parent().on_stat_card_clicked(self.title_text)
super().mousePressEvent(event)
class ServiceStatusCard(QFrame):
def __init__(self, service_name: str, parent=None):
super().__init__(parent)
self.service_name = service_name
self.setup_ui()
def setup_ui(self):
self.setFixedHeight(140)
self.setStyleSheet("""
ServiceStatusCard {
background: #282828;
border-radius: 8px;
border: 1px solid #404040;
}
ServiceStatusCard:hover {
background: #333333;
}
""")
layout.addWidget(title_label)
layout.addWidget(value_label)
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 12, 15, 12)
layout.setSpacing(8)
# Header with service name and status indicator
header_layout = QHBoxLayout()
header_layout.setSpacing(10)
self.service_label = QLabel(self.service_name)
self.service_label.setFont(QFont("Arial", 12, QFont.Weight.Bold))
self.service_label.setStyleSheet("color: #ffffff;")
self.status_indicator = QLabel("")
self.status_indicator.setFont(QFont("Arial", 16))
self.status_indicator.setStyleSheet("color: #ff4444;") # Red by default
header_layout.addWidget(self.service_label)
header_layout.addStretch()
header_layout.addWidget(self.status_indicator)
# Status details
self.status_text = QLabel("Disconnected")
self.status_text.setFont(QFont("Arial", 9))
self.status_text.setStyleSheet("color: #b3b3b3;")
self.response_time_label = QLabel("Response: --")
self.response_time_label.setFont(QFont("Arial", 8))
self.response_time_label.setStyleSheet("color: #888888;")
# Test connection button
self.test_button = QPushButton("Test Connection")
self.test_button.setFixedHeight(24)
self.test_button.setFont(QFont("Arial", 8))
self.test_button.setStyleSheet("""
QPushButton {
background: #1db954;
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
}
QPushButton:hover {
background: #1ed760;
}
QPushButton:pressed {
background: #169c46;
}
QPushButton:disabled {
background: #555555;
color: #999999;
}
""")
layout.addLayout(header_layout)
layout.addWidget(self.status_text)
layout.addWidget(self.response_time_label)
layout.addStretch()
layout.addWidget(self.test_button)
def update_status(self, connected: bool, response_time: float = 0.0, error: str = ""):
if connected:
self.status_indicator.setStyleSheet("color: #1db954;") # Green
self.status_text.setText("Connected")
self.response_time_label.setText(f"Response: {response_time:.0f}ms")
else:
self.status_indicator.setStyleSheet("color: #ff4444;") # Red
self.status_text.setText("Disconnected")
if error:
self.status_text.setText(f"Error: {error[:30]}..." if len(error) > 30 else f"Error: {error}")
self.response_time_label.setText("Response: --")
# Brief visual feedback
self.test_button.setText("Testing..." if not connected and error == "" else "Test Connection")
self.test_button.setEnabled(True)
class MetadataUpdaterWidget(QFrame):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
self.setStyleSheet("""
MetadataUpdaterWidget {
background: #282828;
border-radius: 8px;
border: 1px solid #404040;
}
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 15, 20, 15)
layout.setSpacing(12)
# Header
header_label = QLabel("Plex Metadata Updater")
header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold))
header_label.setStyleSheet("color: #ffffff;")
# Control section
control_layout = QHBoxLayout()
control_layout.setSpacing(15)
self.start_button = QPushButton("Begin Metadata Update")
self.start_button.setFixedHeight(36)
self.start_button.setFont(QFont("Arial", 10, QFont.Weight.Medium))
self.start_button.setStyleSheet("""
QPushButton {
background: #1db954;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
}
QPushButton:hover {
background: #1ed760;
}
QPushButton:pressed {
background: #169c46;
}
QPushButton:disabled {
background: #555555;
color: #999999;
}
""")
# Current artist display
artist_info_layout = QVBoxLayout()
current_label = QLabel("Current Artist:")
current_label.setFont(QFont("Arial", 9))
current_label.setStyleSheet("color: #b3b3b3;")
self.current_artist_label = QLabel("Not running")
self.current_artist_label.setFont(QFont("Arial", 11, QFont.Weight.Medium))
self.current_artist_label.setStyleSheet("color: #ffffff;")
artist_info_layout.addWidget(current_label)
artist_info_layout.addWidget(self.current_artist_label)
control_layout.addWidget(self.start_button)
control_layout.addLayout(artist_info_layout)
control_layout.addStretch()
# Progress section
progress_layout = QVBoxLayout()
progress_layout.setSpacing(8)
progress_info_layout = QHBoxLayout()
self.progress_label = QLabel("Progress: 0%")
self.progress_label.setFont(QFont("Arial", 10))
self.progress_label.setStyleSheet("color: #ffffff;")
self.count_label = QLabel("0 / 0 artists")
self.count_label.setFont(QFont("Arial", 9))
self.count_label.setStyleSheet("color: #b3b3b3;")
progress_info_layout.addWidget(self.progress_label)
progress_info_layout.addStretch()
progress_info_layout.addWidget(self.count_label)
self.progress_bar = QProgressBar()
self.progress_bar.setFixedHeight(8)
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: none;
border-radius: 4px;
background: #555555;
}
QProgressBar::chunk {
background: #1db954;
border-radius: 4px;
}
""")
progress_layout.addLayout(progress_info_layout)
progress_layout.addWidget(self.progress_bar)
layout.addWidget(header_label)
layout.addLayout(control_layout)
layout.addLayout(progress_layout)
def update_progress(self, is_running: bool, current_artist: str, processed: int, total: int, percentage: float):
if is_running:
self.start_button.setText("Stop Update")
self.start_button.setEnabled(True)
self.current_artist_label.setText(current_artist if current_artist else "Initializing...")
self.progress_label.setText(f"Progress: {percentage:.1f}%")
self.count_label.setText(f"{processed} / {total} artists")
self.progress_bar.setValue(int(percentage))
else:
self.start_button.setText("Begin Metadata Update")
self.start_button.setEnabled(True)
self.current_artist_label.setText("Not running")
self.progress_label.setText("Progress: 0%")
self.count_label.setText("0 / 0 artists")
self.progress_bar.setValue(0)
class ActivityItem(QWidget):
def __init__(self, icon: str, title: str, subtitle: str, time: str, parent=None):
@ -101,8 +528,30 @@ class ActivityItem(QWidget):
class DashboardPage(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# Initialize data provider
self.data_provider = DashboardDataProvider()
self.data_provider.service_status_updated.connect(self.on_service_status_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.sync_progress_updated.connect(self.on_sync_progress_updated)
# Service status cards
self.service_cards = {}
# Stats cards
self.stats_cards = {}
self.setup_ui()
def set_service_clients(self, spotify_client, plex_client, soulseek_client):
"""Called from main window to provide service client references"""
self.data_provider.set_service_clients(spotify_client, plex_client, soulseek_client)
def set_page_references(self, downloads_page, sync_page):
"""Called from main window to provide page references for live data"""
self.data_provider.set_page_references(downloads_page, sync_page)
def setup_ui(self):
self.setStyleSheet("""
DashboardPage {
@ -110,23 +559,65 @@ class DashboardPage(QWidget):
}
""")
main_layout = QVBoxLayout(self)
# Main scroll area
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background: #191414;
}
QScrollBar:vertical {
background: #333333;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background: #555555;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background: #666666;
}
""")
# Scroll content widget
scroll_content = QWidget()
scroll_area.setWidget(scroll_content)
main_layout = QVBoxLayout(scroll_content)
main_layout.setContentsMargins(30, 30, 30, 30)
main_layout.setSpacing(30)
main_layout.setSpacing(25)
# Header
header = self.create_header()
main_layout.addWidget(header)
# Stats grid
stats_grid = self.create_stats_grid()
main_layout.addWidget(stats_grid)
# Service Status Section
service_section = self.create_service_status_section()
main_layout.addWidget(service_section)
# System Stats Section
stats_section = self.create_stats_section()
main_layout.addWidget(stats_section)
# Recent activity
# Plex Metadata Updater
metadata_section = self.create_metadata_section()
main_layout.addWidget(metadata_section)
# Recent Activity
activity_section = self.create_activity_section()
main_layout.addWidget(activity_section)
main_layout.addStretch()
# Set main layout
page_layout = QVBoxLayout(self)
page_layout.setContentsMargins(0, 0, 0, 0)
page_layout.addWidget(scroll_area)
def create_header(self):
header = QWidget()
@ -135,12 +626,12 @@ class DashboardPage(QWidget):
layout.setSpacing(5)
# Welcome message
welcome_label = QLabel("Welcome back!")
welcome_label = QLabel("System Dashboard")
welcome_label.setFont(QFont("Arial", 28, QFont.Weight.Bold))
welcome_label.setStyleSheet("color: #ffffff;")
# Subtitle
subtitle_label = QLabel("Here's what's happening with your music library")
subtitle_label = QLabel("Monitor your music system health and manage operations")
subtitle_label.setFont(QFont("Arial", 14))
subtitle_label.setStyleSheet("color: #b3b3b3;")
@ -149,26 +640,87 @@ class DashboardPage(QWidget):
return header
def create_stats_grid(self):
stats_widget = QWidget()
grid = QGridLayout(stats_widget)
grid.setSpacing(20)
# Sample stats - these will be populated with real data later
stats = [
("Spotify Playlists", "12", "3 synced today"),
("Plex Tracks", "2,847", "156 added this week"),
("Missing Tracks", "23", "Ready to download"),
("Artists Scanned", "89", "Metadata updated"),
("Downloads", "5", "In progress"),
("Sync Status", "98%", "Up to date")
def create_service_status_section(self):
section = QWidget()
layout = QVBoxLayout(section)
layout.setSpacing(15)
# Section header
header_label = QLabel("Service Status")
header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
header_label.setStyleSheet("color: #ffffff;")
# Service cards grid
cards_layout = QHBoxLayout()
cards_layout.setSpacing(20)
# Create service status cards
services = ['Spotify', 'Plex', 'Soulseek']
for service in services:
card = ServiceStatusCard(service)
card.test_button.clicked.connect(lambda checked, s=service.lower(): self.test_service_connection(s))
self.service_cards[service.lower()] = card
cards_layout.addWidget(card)
cards_layout.addStretch()
layout.addWidget(header_label)
layout.addLayout(cards_layout)
return section
def create_stats_section(self):
section = QWidget()
layout = QVBoxLayout(section)
layout.setSpacing(15)
# Section header
header_label = QLabel("System Statistics")
header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
header_label.setStyleSheet("color: #ffffff;")
# Stats grid
stats_grid = QGridLayout()
stats_grid.setSpacing(20)
# Create stats cards
stats_data = [
("Active Downloads", "0", "Currently downloading", "active_downloads"),
("Finished Downloads", "0", "Completed today", "finished_downloads"),
("Download Speed", "0 KB/s", "Combined speed", "download_speed"),
("Active Syncs", "0", "Playlists syncing", "active_syncs"),
("System Uptime", "0m", "Application runtime", "uptime"),
("Memory Usage", "--", "Current usage", "memory")
]
for i, (title, value, subtitle) in enumerate(stats):
card = StatCard(title, value, subtitle)
grid.addWidget(card, i // 3, i % 3)
for i, (title, value, subtitle, key) in enumerate(stats_data):
card = StatCard(title, value, subtitle, clickable=False)
self.stats_cards[key] = card
stats_grid.addWidget(card, i // 3, i % 3)
return stats_widget
layout.addWidget(header_label)
layout.addLayout(stats_grid)
return section
def create_metadata_section(self):
section = QWidget()
layout = QVBoxLayout(section)
layout.setSpacing(15)
# Section header
header_label = QLabel("Tools & Operations")
header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
header_label.setStyleSheet("color: #ffffff;")
# Metadata updater widget
self.metadata_widget = MetadataUpdaterWidget()
self.metadata_widget.start_button.clicked.connect(self.toggle_metadata_update)
layout.addWidget(header_label)
layout.addWidget(self.metadata_widget)
return section
def create_activity_section(self):
activity_widget = QWidget()
@ -194,27 +746,139 @@ class DashboardPage(QWidget):
activity_layout.setContentsMargins(0, 0, 0, 0)
activity_layout.setSpacing(1)
# Sample activity items
activities = [
("🔄", "Playlist Sync", "Synced 'Favorites' playlist to Plex", "2 min ago"),
("📥", "Download Complete", "Downloaded 'Song Title' by Artist", "5 min ago"),
("🎵", "Artist Updated", "Updated metadata for 'Artist Name'", "1 hour ago"),
("", "Sync Complete", "All playlists synchronized successfully", "3 hours ago"),
("📊", "Library Scan", "Scanned 156 new tracks in Plex", "1 day ago")
]
# Activity feed will be populated dynamically
self.activity_layout = activity_layout
for icon, title, subtitle, time in activities:
item = ActivityItem(icon, title, subtitle, time)
activity_layout.addWidget(item)
# Add separator (except for last item)
if (icon, title, subtitle, time) != activities[-1]:
separator = QFrame()
separator.setFixedHeight(1)
separator.setStyleSheet("background: #404040;")
activity_layout.addWidget(separator)
# Add initial placeholder
placeholder_item = ActivityItem("📊", "System Started", "Dashboard initialized successfully", "Now")
activity_layout.addWidget(placeholder_item)
layout.addWidget(header_label)
layout.addWidget(activity_container)
return activity_widget
return activity_widget
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)
# Update status to testing state
card.status_indicator.setStyleSheet("color: #ffaa00;") # Orange
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):
"""Toggle metadata update process"""
# This will be implemented with actual functionality later
# For now, just show placeholder behavior
current_text = self.metadata_widget.start_button.text()
if "Begin" in current_text:
# Start metadata update (placeholder)
self.metadata_widget.update_progress(True, "Sample Artist", 0, 100, 0.0)
self.add_activity_item("🎵", "Metadata Update", "Started Plex metadata update process", "Now")
else:
# Stop metadata update (placeholder)
self.metadata_widget.update_progress(False, "", 0, 0, 0.0)
self.add_activity_item("⏹️", "Metadata Update", "Stopped metadata update process", "Now")
def on_service_status_updated(self, service: str, connected: bool, response_time: float, error: str):
"""Handle service status updates from data provider"""
if service in self.service_cards:
self.service_cards[service].update_status(connected, response_time, error)
# Add activity item for status change
status = "Connected" if connected else "Disconnected"
icon = "" if connected else ""
self.add_activity_item(icon, f"{service.capitalize()} {status}",
f"Response time: {response_time:.0f}ms" if connected else f"Error: {error}" if error else "Connection test completed",
"Now")
def on_download_stats_updated(self, active_count: int, finished_count: int, total_speed: float):
"""Handle download statistics updates"""
if 'active_downloads' in self.stats_cards:
self.stats_cards['active_downloads'].update_values(str(active_count), "Currently downloading")
if 'finished_downloads' in self.stats_cards:
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
speed_text = f"{total_speed / (1024 * 1024):.1f} MB/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"
self.stats_cards['download_speed'].update_values(speed_text, "Combined speed")
def on_metadata_progress_updated(self, is_running: bool, current_artist: str, processed: int, total: int, percentage: float):
"""Handle metadata update progress"""
self.metadata_widget.update_progress(is_running, current_artist, processed, total, percentage)
def on_sync_progress_updated(self, current_playlist: str, active_syncs: int):
"""Handle sync progress updates"""
if 'active_syncs' in self.stats_cards:
self.stats_cards['active_syncs'].update_values(str(active_syncs), "Playlists syncing")
def on_stat_card_clicked(self, card_title: str):
"""Handle stat card clicks for detailed views"""
# This can be implemented later for detailed views
pass
def add_activity_item(self, icon: str, title: str, subtitle: str, time_ago: str = "Now"):
"""Add new activity item to the feed"""
# Remove placeholder if it exists
if self.activity_layout.count() == 1:
item = self.activity_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Add separator if there are existing items
if self.activity_layout.count() > 0:
separator = QFrame()
separator.setFixedHeight(1)
separator.setStyleSheet("background: #404040;")
self.activity_layout.insertWidget(0, separator)
# Add new activity item at the top
new_item = ActivityItem(icon, title, subtitle, time_ago)
self.activity_layout.insertWidget(0, new_item)
# Limit to 5 most recent items
while self.activity_layout.count() > 9: # 5 items + 4 separators
item = self.activity_layout.takeAt(self.activity_layout.count() - 1)
if item.widget():
item.widget().deleteLater()
def closeEvent(self, event):
"""Clean up threads when dashboard is closed"""
self.cleanup_threads()
super().closeEvent(event)
def cleanup_threads(self):
"""Clean up all running test threads"""
if hasattr(self.data_provider, '_test_threads'):
for service, thread in self.data_provider._test_threads.items():
if thread.isRunning():
thread.quit()
thread.wait(1000) # Wait up to 1 second
thread.deleteLater()
self.data_provider._test_threads.clear()
# Stop the data provider timer
if hasattr(self.data_provider, 'download_stats_timer'):
self.data_provider.download_stats_timer.stop()
Loading…
Cancel
Save