From 135728ce898e3ccf5794698df73a8c8ede928c03 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Wed, 10 Sep 2025 15:22:31 -0700 Subject: [PATCH] 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. --- web_server.py | 118 +++++++++++++++++++++++++++++++++++++++-- webui/static/script.js | 79 +++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/web_server.py b/web_server.py index 34e86c0b..cd64a6e9 100644 --- a/web_server.py +++ b/web_server.py @@ -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) diff --git a/webui/static/script.js b/webui/static/script.js index 7b930b63..447d4145 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -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 --- /**