From d5f7b1598fd5d5cb720441faa728d33f392a4823 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 1 Nov 2025 19:23:32 +0100 Subject: [PATCH] use of websockets --- .vscode/launch.json | 7 ++ data/download_queue.json | 82 +----------- src/core/SeriesApp.py | 15 ++- src/server/web/static/js/queue.js | 125 ++++++++++++++++--- src/server/web/static/js/websocket_client.js | 2 + 5 files changed, 131 insertions(+), 100 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6989bff..75622d3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,7 @@ "program": "${workspaceFolder}/src/server/fastapi_app.py", "console": "integratedTerminal", "justMyCode": true, + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "env": { "PYTHONPATH": "${workspaceFolder}/src:${workspaceFolder}", "JWT_SECRET_KEY": "your-secret-key-here-debug", @@ -29,6 +30,7 @@ "type": "debugpy", "request": "launch", "module": "uvicorn", + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "args": [ "src.server.fastapi_app:app", "--host", @@ -59,6 +61,7 @@ "program": "${workspaceFolder}/src/cli/Main.py", "console": "integratedTerminal", "justMyCode": true, + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "env": { "PYTHONPATH": "${workspaceFolder}/src:${workspaceFolder}", "LOG_LEVEL": "DEBUG", @@ -76,6 +79,7 @@ "type": "debugpy", "request": "launch", "module": "pytest", + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "args": [ "${workspaceFolder}/tests", "-v", @@ -101,6 +105,7 @@ "type": "debugpy", "request": "launch", "module": "pytest", + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "args": [ "${workspaceFolder}/tests/unit", "-v", @@ -121,6 +126,7 @@ "type": "debugpy", "request": "launch", "module": "pytest", + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "args": [ "${workspaceFolder}/tests/integration", "-v", @@ -144,6 +150,7 @@ "type": "debugpy", "request": "launch", "module": "uvicorn", + "python": "/home/lukas/miniconda3/envs/AniWorld/bin/python", "args": [ "src.server.fastapi_app:app", "--host", diff --git a/data/download_queue.json b/data/download_queue.json index e75f079..6e38f71 100644 --- a/data/download_queue.json +++ b/data/download_queue.json @@ -1,85 +1,5 @@ { "pending": [ - { - "id": "5b0561c2-7a15-4ca8-8e7e-6216ece88ed9", - "serie_id": "highschool-dxd", - "serie_folder": "Highschool DxD", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 3, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-01T17:50:20.472293Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "d1df1aae-5c42-4fdd-89eb-ebf0f3616834", - "serie_id": "highschool-dxd", - "serie_folder": "Highschool DxD", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 4, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-01T17:50:20.472324Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "9c3cee02-ce8d-4d6e-9f55-72f171b7c062", - "serie_id": "highschool-dxd", - "serie_folder": "Highschool DxD", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 5, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-01T17:50:20.472353Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "118189fe-f600-45de-a51a-5a4b96b07f49", - "serie_id": "highschool-dxd", - "serie_folder": "Highschool DxD", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 6, - "title": null - }, - "status": "pending", - "priority": "NORMAL", - "added_at": "2025-11-01T17:50:20.472386Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, { "id": "1ee2224c-24bf-46ea-b577-30d04ce8ecb8", "serie_id": "highschool-dxd", @@ -943,5 +863,5 @@ ], "active": [], "failed": [], - "timestamp": "2025-11-01T17:55:22.905727+00:00" + "timestamp": "2025-11-01T18:23:07.297144+00:00" } \ No newline at end of file diff --git a/src/core/SeriesApp.py b/src/core/SeriesApp.py index 037d5eb..57705dd 100644 --- a/src/core/SeriesApp.py +++ b/src/core/SeriesApp.py @@ -251,8 +251,17 @@ class SeriesApp: raise InterruptedError("Download cancelled by user") # yt-dlp passes a dict with progress information - # Extract percentage from the dict + # Only process progress updates when status is 'downloading' + # (yt-dlp also sends 'finished', 'error', etc.) if isinstance(progress_info, dict): + status = progress_info.get('status') + if status and status != 'downloading': + logger.debug( + f"Skipping progress update with status: {status}" + ) + return + + # Extract percentage from the dict # Calculate percentage based on downloaded/total bytes downloaded = progress_info.get('downloaded_bytes', 0) total_bytes = ( @@ -325,7 +334,9 @@ class SeriesApp: # Call callback with web API format # (dict with detailed progress info) if callback: - logger.debug(f"Calling progress callback: {web_progress_dict}") + logger.debug( + f"Calling progress callback: {web_progress_dict}" + ) try: callback(web_progress_dict) logger.debug("Progress callback executed successfully") diff --git a/src/server/web/static/js/queue.js b/src/server/web/static/js/queue.js index 7080547..e921ca3 100644 --- a/src/server/web/static/js/queue.js +++ b/src/server/web/static/js/queue.js @@ -14,8 +14,9 @@ class QueueManager { this.initSocket(); this.bindEvents(); this.initTheme(); - this.startRefreshTimer(); - this.loadQueueData(); + // Remove polling - use WebSocket events for real-time updates + // this.startRefreshTimer(); // ← REMOVED + this.loadQueueData(); // Load initial data once } initSocket() { @@ -54,24 +55,22 @@ class QueueManager { } }); - this.socket.on('download_progress_update', (data) => { - // Progress updates trigger a data reload to refresh the UI - this.loadQueueData(); - }); - // Download queue events this.socket.on('download_started', () => { this.showToast('Download queue started', 'success'); - this.loadQueueData(); // Refresh data + // Full reload needed - queue structure changed + this.loadQueueData(); }); this.socket.on('queue_started', () => { this.showToast('Download queue started', 'success'); - this.loadQueueData(); // Refresh data + // Full reload needed - queue structure changed + this.loadQueueData(); }); this.socket.on('download_progress', (data) => { - // Progress updates trigger a data reload to refresh the UI - this.loadQueueData(); + // Update progress in real-time without reloading all data + console.log('Received download progress:', data); + this.updateDownloadProgress(data); }); // Handle both old and new download completion events @@ -79,7 +78,8 @@ class QueueManager { const serieName = data.serie_name || data.serie || 'Unknown'; const episode = data.episode || ''; this.showToast(`Completed: ${serieName}${episode ? ' - Episode ' + episode : ''}`, 'success'); - this.loadQueueData(); // Refresh data + // Full reload needed - item moved from active to completed + this.loadQueueData(); }; this.socket.on('download_completed', handleDownloadComplete); this.socket.on('download_complete', handleDownloadComplete); @@ -88,19 +88,22 @@ class QueueManager { const handleDownloadError = (data) => { const message = data.error || data.message || 'Unknown error'; this.showToast(`Download failed: ${message}`, 'error'); - this.loadQueueData(); // Refresh data + // Full reload needed - item moved from active to failed + this.loadQueueData(); }; this.socket.on('download_error', handleDownloadError); this.socket.on('download_failed', handleDownloadError); this.socket.on('download_queue_completed', () => { this.showToast('All downloads completed!', 'success'); - this.loadQueueData(); // Refresh data + // Full reload needed - queue state changed + this.loadQueueData(); }); this.socket.on('queue_completed', () => { this.showToast('All downloads completed!', 'success'); - this.loadQueueData(); // Refresh data + // Full reload needed - queue state changed + this.loadQueueData(); }); this.socket.on('download_stop_requested', () => { @@ -110,7 +113,8 @@ class QueueManager { // Handle both old and new queue stopped events const handleQueueStopped = () => { this.showToast('Download queue stopped', 'success'); - this.loadQueueData(); // Refresh data + // Full reload needed - queue state changed + this.loadQueueData(); }; this.socket.on('download_stopped', handleQueueStopped); this.socket.on('queue_stopped', handleQueueStopped); @@ -118,11 +122,13 @@ class QueueManager { // Handle queue paused/resumed this.socket.on('queue_paused', () => { this.showToast('Queue paused', 'info'); + // Full reload needed - queue state changed this.loadQueueData(); }); this.socket.on('queue_resumed', () => { this.showToast('Queue resumed', 'success'); + // Full reload needed - queue state changed this.loadQueueData(); }); } @@ -273,6 +279,91 @@ class QueueManager { } } + /** + * Update download progress in real-time + * @param {Object} data - Progress data from WebSocket + */ + updateDownloadProgress(data) { + console.log('updateDownloadProgress called with:', JSON.stringify(data, null, 2)); + + // Extract download ID - handle different data structures + let downloadId = data.id || data.download_id || data.item_id; + + // Check if data is wrapped in another 'data' property + if (!downloadId && data.data) { + downloadId = data.data.id || data.data.download_id || data.data.item_id; + data = data.data; // Use nested data + } + + // Also try metadata.item_id as fallback + if (!downloadId && data.metadata && data.metadata.item_id) { + downloadId = data.metadata.item_id; + } + + if (!downloadId) { + console.warn('No download ID in progress data'); + console.warn('Data structure:', data); + console.warn('Available keys:', Object.keys(data)); + return; + } + + // Find the download card in active downloads + const card = document.querySelector(`[data-download-id="${downloadId}"]`); + if (!card) { + // Card not found - might need to reload queue to get new active download + console.log(`Download card not found for ID: ${downloadId}, reloading queue`); + this.loadQueueData(); + return; + } + + // Extract progress information - handle both ProgressService and yt-dlp formats + const progress = data.progress || data; + const percent = progress.percent || 0; + + // Check if we have detailed yt-dlp progress (downloaded_mb, total_mb, speed_mbps) + // or basic ProgressService progress (current, total) + let downloaded, total, speed; + + if (progress.downloaded_mb !== undefined && progress.total_mb !== undefined) { + // yt-dlp detailed format + downloaded = progress.downloaded_mb.toFixed(1); + total = progress.total_mb.toFixed(1); + speed = progress.speed_mbps ? progress.speed_mbps.toFixed(1) : '0.0'; + } else if (progress.current !== undefined && progress.total !== undefined) { + // ProgressService basic format - convert bytes to MB + downloaded = (progress.current / (1024 * 1024)).toFixed(1); + total = progress.total > 0 ? (progress.total / (1024 * 1024)).toFixed(1) : 'Unknown'; + speed = '0.0'; // Speed not available in basic format + } else { + // Fallback + downloaded = '0.0'; + total = 'Unknown'; + speed = '0.0'; + } + + // Update progress bar + const progressFill = card.querySelector('.progress-fill'); + if (progressFill) { + progressFill.style.width = `${percent}%`; + } + + // Update progress text + const progressInfo = card.querySelector('.progress-info'); + if (progressInfo) { + const percentSpan = progressInfo.querySelector('span:first-child'); + const speedSpan = progressInfo.querySelector('.download-speed'); + + if (percentSpan) { + percentSpan.textContent = `${percent.toFixed(1)}% (${downloaded} MB / ${total} MB)`; + } + if (speedSpan) { + speedSpan.textContent = `${speed} MB/s`; + } + } + + console.log(`Updated progress for ${downloadId}: ${percent.toFixed(1)}%`); + } + renderActiveDownloads(downloads) { const container = document.getElementById('active-downloads'); @@ -297,7 +388,7 @@ class QueueManager { const total = progress.total_mb ? `${progress.total_mb.toFixed(1)} MB` : 'Unknown'; return ` -
+

${this.escapeHtml(download.serie_name)}

diff --git a/src/server/web/static/js/websocket_client.js b/src/server/web/static/js/websocket_client.js index 75d23d2..f63f2d5 100644 --- a/src/server/web/static/js/websocket_client.js +++ b/src/server/web/static/js/websocket_client.js @@ -102,6 +102,8 @@ class WebSocketClient { const message = JSON.parse(data); const { type, data: payload, timestamp } = message; + console.log(`WebSocket message: type=${type}`, payload); + // Emit event with payload if (type) { this.emit(type, payload || {});