Add system stats API and dashboard service status updates

Introduces a new /api/system/stats endpoint in web_server.py to provide system statistics such as uptime, memory usage, download speed, and sync counts. Enhances the dashboard in script.js to periodically fetch and display service connection status and system stats, including response times for Spotify, media server, and Soulseek.
This commit is contained in:
Broque Thomas 2025-09-10 15:22:31 -07:00
parent 8336109661
commit 135728ce89
2 changed files with 194 additions and 3 deletions

View file

@ -1318,23 +1318,131 @@ def get_status():
if not all([spotify_client, plex_client, jellyfin_client, soulseek_client, config_manager]):
return jsonify({"error": "Core services not initialized."}), 500
try:
import time
active_server = config_manager.get_active_media_server()
# Test Spotify with response time
spotify_start = time.time()
spotify_status = spotify_client.is_authenticated()
spotify_response_time = (time.time() - spotify_start) * 1000 # Convert to ms
# Test media server with response time
media_server_start = time.time()
media_server_status = False
if active_server == "plex":
media_server_status = plex_client.is_connected()
elif active_server == "jellyfin":
media_server_status = jellyfin_client.is_connected()
media_server_response_time = (time.time() - media_server_start) * 1000
# Test Soulseek (just check if configured, no network test)
soulseek_start = time.time()
soulseek_status = soulseek_client.is_configured()
soulseek_response_time = (time.time() - soulseek_start) * 1000
status_data = {
'spotify': spotify_client.is_authenticated(),
'media_server': media_server_status,
'soulseek': soulseek_client.is_configured(),
'spotify': {
'connected': spotify_status,
'response_time': round(spotify_response_time, 1)
},
'media_server': {
'connected': media_server_status,
'response_time': round(media_server_response_time, 1),
'type': active_server
},
'soulseek': {
'connected': soulseek_status,
'response_time': round(soulseek_response_time, 1)
},
'active_media_server': active_server
}
return jsonify(status_data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/system/stats')
def get_system_stats():
"""Get system statistics for dashboard"""
try:
import psutil
import time
from datetime import timedelta
# Calculate uptime
start_time = getattr(app, 'start_time', time.time())
uptime_seconds = time.time() - start_time
uptime = str(timedelta(seconds=int(uptime_seconds)))
# Get memory usage
memory = psutil.virtual_memory()
memory_usage = f"{memory.percent}%"
# Count active downloads from download_batches (batches that are currently downloading)
active_downloads = len([batch_id for batch_id, batch_data in download_batches.items()
if batch_data.get('phase') == 'downloading'])
# Count finished downloads (completed this session)
finished_downloads = len([batch_id for batch_id, batch_data in download_batches.items()
if batch_data.get('phase') == 'complete'])
# Calculate total download speed from active soulseek transfers
total_download_speed = 0.0
try:
transfers_data = asyncio.run(soulseek_client._make_request('GET', 'transfers/downloads'))
if transfers_data:
for user_data in transfers_data:
if 'directories' in user_data:
for directory in user_data['directories']:
if 'files' in directory:
for file_info in directory['files']:
state = file_info.get('state', '').lower()
# Only count actively downloading files
if 'inprogress' in state or 'downloading' in state or 'transferring' in state:
speed = file_info.get('averageSpeed', 0)
if isinstance(speed, (int, float)) and speed > 0:
total_download_speed += float(speed)
except Exception as e:
print(f"Warning: Could not fetch download speeds: {e}")
# Convert bytes/sec to KB/s and format
if total_download_speed > 0:
speed_kb_s = total_download_speed / 1024
if speed_kb_s >= 1024:
speed_mb_s = speed_kb_s / 1024
download_speed_str = f"{speed_mb_s:.1f} MB/s"
else:
download_speed_str = f"{speed_kb_s:.1f} KB/s"
else:
download_speed_str = "0 KB/s"
# Count active syncs (playlists currently syncing)
active_syncs = 0
# Count Spotify playlist syncs
for playlist_id, sync_state in sync_states.items():
if sync_state.get('status') == 'syncing':
active_syncs += 1
# Count YouTube playlist syncs
for url_hash, state in youtube_playlist_states.items():
if state.get('phase') == 'syncing':
active_syncs += 1
# Count Tidal playlist syncs
for playlist_id, state in tidal_discovery_states.items():
if state.get('phase') == 'syncing':
active_syncs += 1
stats_data = {
'active_downloads': active_downloads,
'finished_downloads': finished_downloads,
'download_speed': download_speed_str,
'active_syncs': active_syncs,
'uptime': uptime,
'memory_usage': memory_usage
}
return jsonify(stats_data)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/settings', methods=['GET', 'POST'])
def handle_settings():
global tidal_client # Declare that we might modify the global instance
@ -10173,4 +10281,8 @@ if __name__ == '__main__':
start_watchlist_auto_scanning()
print("✅ Automatic watchlist scanning started")
# Initialize app start time for uptime tracking
import time
app.start_time = time.time()
app.run(host='0.0.0.0', port=5001, debug=True)

View file

@ -6882,6 +6882,14 @@ async function loadDashboardData() {
// Start periodic refresh of wishlist count (every 30 seconds, matching GUI behavior)
stopWishlistCountPolling(); // Ensure no duplicates
wishlistCountInterval = setInterval(updateWishlistCount, 30000);
// Initial load of service status and system statistics
await fetchAndUpdateServiceStatus();
await fetchAndUpdateSystemStats();
// Start periodic refresh of service status and system stats (every 10 seconds)
setInterval(fetchAndUpdateServiceStatus, 10000);
setInterval(fetchAndUpdateSystemStats, 10000);
// Also check the status of any ongoing update when the page loads
await checkAndUpdateDbProgress();
@ -12012,6 +12020,77 @@ function escapeHtml(text) {
return div.innerHTML;
}
// --- Service Status and System Stats Functions ---
async function fetchAndUpdateServiceStatus() {
try {
const response = await fetch('/status');
if (!response.ok) return;
const data = await response.json();
// Update service status indicators and text
updateServiceStatus('spotify', data.spotify);
updateServiceStatus('media-server', data.media_server);
updateServiceStatus('soulseek', data.soulseek);
} catch (error) {
console.warn('Could not fetch service status:', error);
}
}
function updateServiceStatus(service, statusData) {
const indicator = document.getElementById(`${service}-status-indicator`);
const statusText = document.getElementById(`${service}-status-text`);
if (indicator && statusText) {
if (statusData.connected) {
indicator.className = 'service-card-indicator connected';
statusText.textContent = `Connected (${statusData.response_time}ms)`;
statusText.className = 'service-card-status-text connected';
} else {
indicator.className = 'service-card-indicator disconnected';
statusText.textContent = 'Disconnected';
statusText.className = 'service-card-status-text disconnected';
}
}
}
async function fetchAndUpdateSystemStats() {
try {
const response = await fetch('/api/system/stats');
if (!response.ok) return;
const data = await response.json();
// Update all stat cards
updateStatCard('active-downloads-card', data.active_downloads, 'Currently downloading');
updateStatCard('finished-downloads-card', data.finished_downloads, 'Completed this session');
updateStatCard('download-speed-card', data.download_speed, 'Combined speed');
updateStatCard('active-syncs-card', data.active_syncs, 'Playlists syncing');
updateStatCard('uptime-card', data.uptime, 'Application runtime');
updateStatCard('memory-card', data.memory_usage, 'Current usage');
} catch (error) {
console.warn('Could not fetch system stats:', error);
}
}
function updateStatCard(cardId, value, subtitle) {
const card = document.getElementById(cardId);
if (card) {
const valueElement = card.querySelector('.stat-card-value');
const subtitleElement = card.querySelector('.stat-card-subtitle');
if (valueElement) {
valueElement.textContent = value;
}
if (subtitleElement) {
subtitleElement.textContent = subtitle;
}
}
}
// --- Watchlist Functions ---
/**