use of websockets
This commit is contained in:
parent
57c30a0156
commit
d5f7b1598f
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@ -8,6 +8,7 @@
|
|||||||
"program": "${workspaceFolder}/src/server/fastapi_app.py",
|
"program": "${workspaceFolder}/src/server/fastapi_app.py",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"env": {
|
"env": {
|
||||||
"PYTHONPATH": "${workspaceFolder}/src:${workspaceFolder}",
|
"PYTHONPATH": "${workspaceFolder}/src:${workspaceFolder}",
|
||||||
"JWT_SECRET_KEY": "your-secret-key-here-debug",
|
"JWT_SECRET_KEY": "your-secret-key-here-debug",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"module": "uvicorn",
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"args": [
|
"args": [
|
||||||
"src.server.fastapi_app:app",
|
"src.server.fastapi_app:app",
|
||||||
"--host",
|
"--host",
|
||||||
@ -59,6 +61,7 @@
|
|||||||
"program": "${workspaceFolder}/src/cli/Main.py",
|
"program": "${workspaceFolder}/src/cli/Main.py",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"env": {
|
"env": {
|
||||||
"PYTHONPATH": "${workspaceFolder}/src:${workspaceFolder}",
|
"PYTHONPATH": "${workspaceFolder}/src:${workspaceFolder}",
|
||||||
"LOG_LEVEL": "DEBUG",
|
"LOG_LEVEL": "DEBUG",
|
||||||
@ -76,6 +79,7 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "pytest",
|
"module": "pytest",
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"args": [
|
"args": [
|
||||||
"${workspaceFolder}/tests",
|
"${workspaceFolder}/tests",
|
||||||
"-v",
|
"-v",
|
||||||
@ -101,6 +105,7 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "pytest",
|
"module": "pytest",
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"args": [
|
"args": [
|
||||||
"${workspaceFolder}/tests/unit",
|
"${workspaceFolder}/tests/unit",
|
||||||
"-v",
|
"-v",
|
||||||
@ -121,6 +126,7 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "pytest",
|
"module": "pytest",
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"args": [
|
"args": [
|
||||||
"${workspaceFolder}/tests/integration",
|
"${workspaceFolder}/tests/integration",
|
||||||
"-v",
|
"-v",
|
||||||
@ -144,6 +150,7 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"module": "uvicorn",
|
||||||
|
"python": "/home/lukas/miniconda3/envs/AniWorld/bin/python",
|
||||||
"args": [
|
"args": [
|
||||||
"src.server.fastapi_app:app",
|
"src.server.fastapi_app:app",
|
||||||
"--host",
|
"--host",
|
||||||
|
|||||||
@ -1,85 +1,5 @@
|
|||||||
{
|
{
|
||||||
"pending": [
|
"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",
|
"id": "1ee2224c-24bf-46ea-b577-30d04ce8ecb8",
|
||||||
"serie_id": "highschool-dxd",
|
"serie_id": "highschool-dxd",
|
||||||
@ -943,5 +863,5 @@
|
|||||||
],
|
],
|
||||||
"active": [],
|
"active": [],
|
||||||
"failed": [],
|
"failed": [],
|
||||||
"timestamp": "2025-11-01T17:55:22.905727+00:00"
|
"timestamp": "2025-11-01T18:23:07.297144+00:00"
|
||||||
}
|
}
|
||||||
@ -251,8 +251,17 @@ class SeriesApp:
|
|||||||
raise InterruptedError("Download cancelled by user")
|
raise InterruptedError("Download cancelled by user")
|
||||||
|
|
||||||
# yt-dlp passes a dict with progress information
|
# 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):
|
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
|
# Calculate percentage based on downloaded/total bytes
|
||||||
downloaded = progress_info.get('downloaded_bytes', 0)
|
downloaded = progress_info.get('downloaded_bytes', 0)
|
||||||
total_bytes = (
|
total_bytes = (
|
||||||
@ -325,7 +334,9 @@ class SeriesApp:
|
|||||||
# Call callback with web API format
|
# Call callback with web API format
|
||||||
# (dict with detailed progress info)
|
# (dict with detailed progress info)
|
||||||
if callback:
|
if callback:
|
||||||
logger.debug(f"Calling progress callback: {web_progress_dict}")
|
logger.debug(
|
||||||
|
f"Calling progress callback: {web_progress_dict}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
callback(web_progress_dict)
|
callback(web_progress_dict)
|
||||||
logger.debug("Progress callback executed successfully")
|
logger.debug("Progress callback executed successfully")
|
||||||
|
|||||||
@ -14,8 +14,9 @@ class QueueManager {
|
|||||||
this.initSocket();
|
this.initSocket();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.initTheme();
|
this.initTheme();
|
||||||
this.startRefreshTimer();
|
// Remove polling - use WebSocket events for real-time updates
|
||||||
this.loadQueueData();
|
// this.startRefreshTimer(); // ← REMOVED
|
||||||
|
this.loadQueueData(); // Load initial data once
|
||||||
}
|
}
|
||||||
|
|
||||||
initSocket() {
|
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
|
// Download queue events
|
||||||
this.socket.on('download_started', () => {
|
this.socket.on('download_started', () => {
|
||||||
this.showToast('Download queue started', 'success');
|
this.showToast('Download queue started', 'success');
|
||||||
this.loadQueueData(); // Refresh data
|
// Full reload needed - queue structure changed
|
||||||
|
this.loadQueueData();
|
||||||
});
|
});
|
||||||
this.socket.on('queue_started', () => {
|
this.socket.on('queue_started', () => {
|
||||||
this.showToast('Download queue started', 'success');
|
this.showToast('Download queue started', 'success');
|
||||||
this.loadQueueData(); // Refresh data
|
// Full reload needed - queue structure changed
|
||||||
|
this.loadQueueData();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('download_progress', (data) => {
|
this.socket.on('download_progress', (data) => {
|
||||||
// Progress updates trigger a data reload to refresh the UI
|
// Update progress in real-time without reloading all data
|
||||||
this.loadQueueData();
|
console.log('Received download progress:', data);
|
||||||
|
this.updateDownloadProgress(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle both old and new download completion events
|
// Handle both old and new download completion events
|
||||||
@ -79,7 +78,8 @@ class QueueManager {
|
|||||||
const serieName = data.serie_name || data.serie || 'Unknown';
|
const serieName = data.serie_name || data.serie || 'Unknown';
|
||||||
const episode = data.episode || '';
|
const episode = data.episode || '';
|
||||||
this.showToast(`Completed: ${serieName}${episode ? ' - Episode ' + episode : ''}`, 'success');
|
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_completed', handleDownloadComplete);
|
||||||
this.socket.on('download_complete', handleDownloadComplete);
|
this.socket.on('download_complete', handleDownloadComplete);
|
||||||
@ -88,19 +88,22 @@ class QueueManager {
|
|||||||
const handleDownloadError = (data) => {
|
const handleDownloadError = (data) => {
|
||||||
const message = data.error || data.message || 'Unknown error';
|
const message = data.error || data.message || 'Unknown error';
|
||||||
this.showToast(`Download failed: ${message}`, '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_error', handleDownloadError);
|
||||||
this.socket.on('download_failed', handleDownloadError);
|
this.socket.on('download_failed', handleDownloadError);
|
||||||
|
|
||||||
this.socket.on('download_queue_completed', () => {
|
this.socket.on('download_queue_completed', () => {
|
||||||
this.showToast('All downloads completed!', 'success');
|
this.showToast('All downloads completed!', 'success');
|
||||||
this.loadQueueData(); // Refresh data
|
// Full reload needed - queue state changed
|
||||||
|
this.loadQueueData();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('queue_completed', () => {
|
this.socket.on('queue_completed', () => {
|
||||||
this.showToast('All downloads completed!', 'success');
|
this.showToast('All downloads completed!', 'success');
|
||||||
this.loadQueueData(); // Refresh data
|
// Full reload needed - queue state changed
|
||||||
|
this.loadQueueData();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('download_stop_requested', () => {
|
this.socket.on('download_stop_requested', () => {
|
||||||
@ -110,7 +113,8 @@ class QueueManager {
|
|||||||
// Handle both old and new queue stopped events
|
// Handle both old and new queue stopped events
|
||||||
const handleQueueStopped = () => {
|
const handleQueueStopped = () => {
|
||||||
this.showToast('Download queue stopped', 'success');
|
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('download_stopped', handleQueueStopped);
|
||||||
this.socket.on('queue_stopped', handleQueueStopped);
|
this.socket.on('queue_stopped', handleQueueStopped);
|
||||||
@ -118,11 +122,13 @@ class QueueManager {
|
|||||||
// Handle queue paused/resumed
|
// Handle queue paused/resumed
|
||||||
this.socket.on('queue_paused', () => {
|
this.socket.on('queue_paused', () => {
|
||||||
this.showToast('Queue paused', 'info');
|
this.showToast('Queue paused', 'info');
|
||||||
|
// Full reload needed - queue state changed
|
||||||
this.loadQueueData();
|
this.loadQueueData();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('queue_resumed', () => {
|
this.socket.on('queue_resumed', () => {
|
||||||
this.showToast('Queue resumed', 'success');
|
this.showToast('Queue resumed', 'success');
|
||||||
|
// Full reload needed - queue state changed
|
||||||
this.loadQueueData();
|
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) {
|
renderActiveDownloads(downloads) {
|
||||||
const container = document.getElementById('active-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';
|
const total = progress.total_mb ? `${progress.total_mb.toFixed(1)} MB` : 'Unknown';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card active">
|
<div class="download-card active" data-download-id="${download.id}">
|
||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
<div class="download-info">
|
<div class="download-info">
|
||||||
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
||||||
|
|||||||
@ -102,6 +102,8 @@ class WebSocketClient {
|
|||||||
const message = JSON.parse(data);
|
const message = JSON.parse(data);
|
||||||
const { type, data: payload, timestamp } = message;
|
const { type, data: payload, timestamp } = message;
|
||||||
|
|
||||||
|
console.log(`WebSocket message: type=${type}`, payload);
|
||||||
|
|
||||||
// Emit event with payload
|
// Emit event with payload
|
||||||
if (type) {
|
if (type) {
|
||||||
this.emit(type, payload || {});
|
this.emit(type, payload || {});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user