refactor: split CSS and JS into modular files (SRP)
This commit is contained in:
189
src/server/web/static/js/queue/progress-handler.js
Normal file
189
src/server/web/static/js/queue/progress-handler.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* AniWorld - Progress Handler Module
|
||||
*
|
||||
* Handles real-time download progress updates.
|
||||
*
|
||||
* Dependencies: constants.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.ProgressHandler = (function() {
|
||||
'use strict';
|
||||
|
||||
// Store progress updates waiting for cards
|
||||
let pendingProgressUpdates = new Map();
|
||||
|
||||
/**
|
||||
* Update download progress in real-time
|
||||
* @param {Object} data - Progress data from WebSocket
|
||||
*/
|
||||
function updateDownloadProgress(data) {
|
||||
console.log('updateDownloadProgress called with:', JSON.stringify(data, null, 2));
|
||||
|
||||
// Extract download ID - prioritize metadata.item_id (actual item ID)
|
||||
let downloadId = null;
|
||||
|
||||
// First try metadata.item_id (this is the actual download item ID)
|
||||
if (data.metadata && data.metadata.item_id) {
|
||||
downloadId = data.metadata.item_id;
|
||||
}
|
||||
|
||||
// Fallback to other ID fields
|
||||
if (!downloadId) {
|
||||
downloadId = data.item_id || data.download_id;
|
||||
}
|
||||
|
||||
// If ID starts with "download_", extract the actual ID
|
||||
if (!downloadId && data.id) {
|
||||
if (data.id.startsWith('download_')) {
|
||||
downloadId = data.id.substring(9);
|
||||
} else {
|
||||
downloadId = data.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if data is wrapped in another 'data' property
|
||||
if (!downloadId && data.data) {
|
||||
if (data.data.metadata && data.data.metadata.item_id) {
|
||||
downloadId = data.data.metadata.item_id;
|
||||
} else if (data.data.item_id) {
|
||||
downloadId = data.data.item_id;
|
||||
} else if (data.data.id && data.data.id.startsWith('download_')) {
|
||||
downloadId = data.data.id.substring(9);
|
||||
} else {
|
||||
downloadId = data.data.id || data.data.download_id;
|
||||
}
|
||||
data = data.data;
|
||||
}
|
||||
|
||||
if (!downloadId) {
|
||||
console.warn('No download ID in progress data');
|
||||
console.warn('Data structure:', data);
|
||||
console.warn('Available keys:', Object.keys(data));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Looking for download card with ID: ' + downloadId);
|
||||
|
||||
// Find the download card in active downloads
|
||||
const card = document.querySelector('[data-download-id="' + downloadId + '"]');
|
||||
if (!card) {
|
||||
console.warn('Download card not found for ID: ' + downloadId);
|
||||
|
||||
// Debug: Log all existing download cards
|
||||
const allCards = document.querySelectorAll('[data-download-id]');
|
||||
console.log('Found ' + allCards.length + ' download cards:');
|
||||
allCards.forEach(function(c) {
|
||||
console.log(' - ' + c.getAttribute('data-download-id'));
|
||||
});
|
||||
|
||||
// Store this progress update to retry after queue loads
|
||||
console.log('Storing progress update for ' + downloadId + ' to retry after reload');
|
||||
pendingProgressUpdates.set(downloadId, data);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Found download card for ID: ' + downloadId + ', updating progress');
|
||||
|
||||
// Extract progress information
|
||||
const progress = data.progress || data;
|
||||
const percent = progress.percent || 0;
|
||||
const metadata = progress.metadata || data.metadata || {};
|
||||
|
||||
// Check data format
|
||||
let speed;
|
||||
|
||||
if (progress.downloaded_mb !== undefined && progress.total_mb !== undefined) {
|
||||
// yt-dlp detailed format
|
||||
speed = progress.speed_mbps ? progress.speed_mbps.toFixed(1) : '0.0';
|
||||
} else if (progress.current !== undefined && progress.total !== undefined) {
|
||||
// ProgressService basic format
|
||||
speed = metadata.speed_mbps ? metadata.speed_mbps.toFixed(1) : '0.0';
|
||||
} else {
|
||||
// Fallback
|
||||
speed = metadata.speed_mbps ? metadata.speed_mbps.toFixed(1) : '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 > 0 ? percent.toFixed(1) + '%' : 'Starting...';
|
||||
}
|
||||
if (speedSpan) {
|
||||
speedSpan.textContent = speed + ' MB/s';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Updated progress for ' + downloadId + ': ' + percent.toFixed(1) + '%');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pending progress updates
|
||||
*/
|
||||
function processPendingProgressUpdates() {
|
||||
if (pendingProgressUpdates.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Processing ' + pendingProgressUpdates.size + ' pending progress updates...');
|
||||
|
||||
// Process each pending update
|
||||
const processed = [];
|
||||
pendingProgressUpdates.forEach(function(data, downloadId) {
|
||||
// Check if card now exists
|
||||
const card = document.querySelector('[data-download-id="' + downloadId + '"]');
|
||||
if (card) {
|
||||
console.log('Retrying progress update for ' + downloadId);
|
||||
updateDownloadProgress(data);
|
||||
processed.push(downloadId);
|
||||
} else {
|
||||
console.log('Card still not found for ' + downloadId + ', will retry on next reload');
|
||||
}
|
||||
});
|
||||
|
||||
// Remove processed updates
|
||||
processed.forEach(function(id) {
|
||||
pendingProgressUpdates.delete(id);
|
||||
});
|
||||
|
||||
if (processed.length > 0) {
|
||||
console.log('Successfully processed ' + processed.length + ' pending updates');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all pending progress updates
|
||||
*/
|
||||
function clearPendingUpdates() {
|
||||
pendingProgressUpdates.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pending update for specific download
|
||||
* @param {string} downloadId - Download ID
|
||||
*/
|
||||
function clearPendingUpdate(downloadId) {
|
||||
pendingProgressUpdates.delete(downloadId);
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
updateDownloadProgress: updateDownloadProgress,
|
||||
processPendingProgressUpdates: processPendingProgressUpdates,
|
||||
clearPendingUpdates: clearPendingUpdates,
|
||||
clearPendingUpdate: clearPendingUpdate
|
||||
};
|
||||
})();
|
||||
159
src/server/web/static/js/queue/queue-api.js
Normal file
159
src/server/web/static/js/queue/queue-api.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* AniWorld - Queue API Module
|
||||
*
|
||||
* Handles API requests for the download queue.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.QueueAPI = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Load queue data from server
|
||||
* @returns {Promise<Object>} Queue data
|
||||
*/
|
||||
async function loadQueueData() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.QUEUE_STATUS);
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// API returns nested structure with 'status' and 'statistics'
|
||||
// Transform it to the expected flat structure
|
||||
return {
|
||||
...data.status,
|
||||
statistics: data.statistics
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading queue data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start queue processing
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async function startQueue() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.post(API.QUEUE_START, {});
|
||||
if (!response) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error starting queue:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop queue processing
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async function stopQueue() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.post(API.QUEUE_STOP, {});
|
||||
if (!response) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error stopping queue:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from queue
|
||||
* @param {string} downloadId - Download item ID
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async function removeFromQueue(downloadId) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.delete(API.QUEUE_REMOVE + '/' + downloadId);
|
||||
if (!response) return false;
|
||||
return response.status === 204;
|
||||
} catch (error) {
|
||||
console.error('Error removing from queue:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry failed downloads
|
||||
* @param {Array<string>} itemIds - Array of download item IDs
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async function retryDownloads(itemIds) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.post(API.QUEUE_RETRY, { item_ids: itemIds });
|
||||
if (!response) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error retrying downloads:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear completed downloads
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async function clearCompleted() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.delete(API.QUEUE_COMPLETED);
|
||||
if (!response) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error clearing completed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear failed downloads
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async function clearFailed() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.delete(API.QUEUE_FAILED);
|
||||
if (!response) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error clearing failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pending downloads
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async function clearPending() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.delete(API.QUEUE_PENDING);
|
||||
if (!response) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error clearing pending:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
loadQueueData: loadQueueData,
|
||||
startQueue: startQueue,
|
||||
stopQueue: stopQueue,
|
||||
removeFromQueue: removeFromQueue,
|
||||
retryDownloads: retryDownloads,
|
||||
clearCompleted: clearCompleted,
|
||||
clearFailed: clearFailed,
|
||||
clearPending: clearPending
|
||||
};
|
||||
})();
|
||||
313
src/server/web/static/js/queue/queue-init.js
Normal file
313
src/server/web/static/js/queue/queue-init.js
Normal file
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* AniWorld - Queue Page Application Initializer
|
||||
*
|
||||
* Main entry point for the queue page. Initializes all modules.
|
||||
*
|
||||
* Dependencies: All shared and queue modules
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.QueueApp = (function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize the queue page application
|
||||
*/
|
||||
async function init() {
|
||||
console.log('AniWorld Queue App initializing...');
|
||||
|
||||
// Check authentication first
|
||||
const isAuthenticated = await AniWorld.Auth.checkAuth();
|
||||
if (!isAuthenticated) {
|
||||
return; // Auth module handles redirect
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
AniWorld.Theme.init();
|
||||
|
||||
// Initialize WebSocket connection
|
||||
AniWorld.WebSocketClient.init();
|
||||
|
||||
// Initialize socket event handlers for this page
|
||||
AniWorld.QueueSocketHandler.init(AniWorld.QueueApp);
|
||||
|
||||
// Bind UI events
|
||||
bindEvents();
|
||||
|
||||
// Load initial data
|
||||
await loadQueueData();
|
||||
|
||||
console.log('AniWorld Queue App initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI event handlers
|
||||
*/
|
||||
function bindEvents() {
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
AniWorld.Theme.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
// Queue management actions
|
||||
const clearCompletedBtn = document.getElementById('clear-completed-btn');
|
||||
if (clearCompletedBtn) {
|
||||
clearCompletedBtn.addEventListener('click', function() {
|
||||
clearQueue('completed');
|
||||
});
|
||||
}
|
||||
|
||||
const clearFailedBtn = document.getElementById('clear-failed-btn');
|
||||
if (clearFailedBtn) {
|
||||
clearFailedBtn.addEventListener('click', function() {
|
||||
clearQueue('failed');
|
||||
});
|
||||
}
|
||||
|
||||
const clearPendingBtn = document.getElementById('clear-pending-btn');
|
||||
if (clearPendingBtn) {
|
||||
clearPendingBtn.addEventListener('click', function() {
|
||||
clearQueue('pending');
|
||||
});
|
||||
}
|
||||
|
||||
const retryAllBtn = document.getElementById('retry-all-btn');
|
||||
if (retryAllBtn) {
|
||||
retryAllBtn.addEventListener('click', retryAllFailed);
|
||||
}
|
||||
|
||||
// Download controls
|
||||
const startQueueBtn = document.getElementById('start-queue-btn');
|
||||
if (startQueueBtn) {
|
||||
startQueueBtn.addEventListener('click', startDownload);
|
||||
}
|
||||
|
||||
const stopQueueBtn = document.getElementById('stop-queue-btn');
|
||||
if (stopQueueBtn) {
|
||||
stopQueueBtn.addEventListener('click', stopDownloads);
|
||||
}
|
||||
|
||||
// Modal events
|
||||
const closeConfirm = document.getElementById('close-confirm');
|
||||
if (closeConfirm) {
|
||||
closeConfirm.addEventListener('click', AniWorld.UI.hideConfirmModal);
|
||||
}
|
||||
|
||||
const confirmCancel = document.getElementById('confirm-cancel');
|
||||
if (confirmCancel) {
|
||||
confirmCancel.addEventListener('click', AniWorld.UI.hideConfirmModal);
|
||||
}
|
||||
|
||||
const modalOverlay = document.querySelector('#confirm-modal .modal-overlay');
|
||||
if (modalOverlay) {
|
||||
modalOverlay.addEventListener('click', AniWorld.UI.hideConfirmModal);
|
||||
}
|
||||
|
||||
// Logout functionality
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', function() {
|
||||
AniWorld.Auth.logout(AniWorld.UI.showToast);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load queue data and update display
|
||||
*/
|
||||
async function loadQueueData() {
|
||||
const data = await AniWorld.QueueAPI.loadQueueData();
|
||||
if (data) {
|
||||
AniWorld.QueueRenderer.updateQueueDisplay(data);
|
||||
AniWorld.ProgressHandler.processPendingProgressUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear queue by type
|
||||
* @param {string} type - 'completed', 'failed', or 'pending'
|
||||
*/
|
||||
async function clearQueue(type) {
|
||||
const titles = {
|
||||
completed: 'Clear Completed Downloads',
|
||||
failed: 'Clear Failed Downloads',
|
||||
pending: 'Remove All Pending Downloads'
|
||||
};
|
||||
|
||||
const messages = {
|
||||
completed: 'Are you sure you want to clear all completed downloads?',
|
||||
failed: 'Are you sure you want to clear all failed downloads?',
|
||||
pending: 'Are you sure you want to remove all pending downloads from the queue?'
|
||||
};
|
||||
|
||||
const confirmed = await AniWorld.UI.showConfirmModal(titles[type], messages[type]);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
let data;
|
||||
if (type === 'completed') {
|
||||
data = await AniWorld.QueueAPI.clearCompleted();
|
||||
AniWorld.UI.showToast('Cleared ' + (data?.count || 0) + ' completed downloads', 'success');
|
||||
} else if (type === 'failed') {
|
||||
data = await AniWorld.QueueAPI.clearFailed();
|
||||
AniWorld.UI.showToast('Cleared ' + (data?.count || 0) + ' failed downloads', 'success');
|
||||
} else if (type === 'pending') {
|
||||
data = await AniWorld.QueueAPI.clearPending();
|
||||
AniWorld.UI.showToast('Removed ' + (data?.count || 0) + ' pending downloads', 'success');
|
||||
}
|
||||
await loadQueueData();
|
||||
} catch (error) {
|
||||
console.error('Error clearing queue:', error);
|
||||
AniWorld.UI.showToast('Failed to clear queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a failed download
|
||||
* @param {string} downloadId - Download item ID
|
||||
*/
|
||||
async function retryDownload(downloadId) {
|
||||
try {
|
||||
const data = await AniWorld.QueueAPI.retryDownloads([downloadId]);
|
||||
AniWorld.UI.showToast('Retried ' + (data?.retried_count || 1) + ' download(s)', 'success');
|
||||
await loadQueueData();
|
||||
} catch (error) {
|
||||
console.error('Error retrying download:', error);
|
||||
AniWorld.UI.showToast('Failed to retry download', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry all failed downloads
|
||||
*/
|
||||
async function retryAllFailed() {
|
||||
const confirmed = await AniWorld.UI.showConfirmModal(
|
||||
'Retry All Failed Downloads',
|
||||
'Are you sure you want to retry all failed downloads?'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
// Get all failed download IDs
|
||||
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
||||
const itemIds = Array.from(failedCards).map(function(card) {
|
||||
return card.dataset.id;
|
||||
}).filter(function(id) {
|
||||
return id;
|
||||
});
|
||||
|
||||
if (itemIds.length === 0) {
|
||||
AniWorld.UI.showToast('No failed downloads to retry', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await AniWorld.QueueAPI.retryDownloads(itemIds);
|
||||
AniWorld.UI.showToast('Retried ' + (data?.retried_count || itemIds.length) + ' download(s)', 'success');
|
||||
await loadQueueData();
|
||||
} catch (error) {
|
||||
console.error('Error retrying failed downloads:', error);
|
||||
AniWorld.UI.showToast('Failed to retry downloads', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from queue
|
||||
* @param {string} downloadId - Download item ID
|
||||
*/
|
||||
async function removeFromQueue(downloadId) {
|
||||
try {
|
||||
const success = await AniWorld.QueueAPI.removeFromQueue(downloadId);
|
||||
if (success) {
|
||||
AniWorld.UI.showToast('Download removed from queue', 'success');
|
||||
await loadQueueData();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to remove from queue', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing from queue:', error);
|
||||
AniWorld.UI.showToast('Failed to remove from queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start queue processing
|
||||
*/
|
||||
async function startDownload() {
|
||||
try {
|
||||
const data = await AniWorld.QueueAPI.startQueue();
|
||||
|
||||
if (data && data.status === 'success') {
|
||||
AniWorld.UI.showToast('Queue processing started - all items will download automatically', 'success');
|
||||
|
||||
// Update UI
|
||||
document.getElementById('start-queue-btn').style.display = 'none';
|
||||
document.getElementById('stop-queue-btn').style.display = 'inline-flex';
|
||||
document.getElementById('stop-queue-btn').disabled = false;
|
||||
|
||||
await loadQueueData();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to start queue: ' + (data?.message || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting queue:', error);
|
||||
AniWorld.UI.showToast('Failed to start queue processing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop queue processing
|
||||
*/
|
||||
async function stopDownloads() {
|
||||
try {
|
||||
const data = await AniWorld.QueueAPI.stopQueue();
|
||||
|
||||
if (data && data.status === 'success') {
|
||||
AniWorld.UI.showToast('Queue processing stopped', 'success');
|
||||
|
||||
// Update UI
|
||||
document.getElementById('stop-queue-btn').style.display = 'none';
|
||||
document.getElementById('start-queue-btn').style.display = 'inline-flex';
|
||||
document.getElementById('start-queue-btn').disabled = false;
|
||||
|
||||
await loadQueueData();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to stop queue: ' + (data?.message || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stopping queue:', error);
|
||||
AniWorld.UI.showToast('Failed to stop queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
loadQueueData: loadQueueData,
|
||||
retryDownload: retryDownload,
|
||||
removeFromQueue: removeFromQueue,
|
||||
startDownload: startDownload,
|
||||
stopDownloads: stopDownloads
|
||||
};
|
||||
})();
|
||||
|
||||
// Initialize the application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
AniWorld.QueueApp.init();
|
||||
});
|
||||
|
||||
// Expose for inline event handlers (backwards compatibility)
|
||||
window.queueManager = {
|
||||
retryDownload: function(id) {
|
||||
return AniWorld.QueueApp.retryDownload(id);
|
||||
},
|
||||
removeFromQueue: function(id) {
|
||||
return AniWorld.QueueApp.removeFromQueue(id);
|
||||
},
|
||||
removeFailedDownload: function(id) {
|
||||
return AniWorld.QueueApp.removeFromQueue(id);
|
||||
}
|
||||
};
|
||||
335
src/server/web/static/js/queue/queue-renderer.js
Normal file
335
src/server/web/static/js/queue/queue-renderer.js
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* AniWorld - Queue Renderer Module
|
||||
*
|
||||
* Handles rendering of queue items and statistics.
|
||||
*
|
||||
* Dependencies: constants.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.QueueRenderer = (function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Update full queue display
|
||||
* @param {Object} data - Queue data
|
||||
*/
|
||||
function updateQueueDisplay(data) {
|
||||
// Update statistics
|
||||
updateStatistics(data.statistics, data);
|
||||
|
||||
// Update active downloads
|
||||
renderActiveDownloads(data.active_downloads || []);
|
||||
|
||||
// Update pending queue
|
||||
renderPendingQueue(data.pending_queue || []);
|
||||
|
||||
// Update completed downloads
|
||||
renderCompletedDownloads(data.completed_downloads || []);
|
||||
|
||||
// Update failed downloads
|
||||
renderFailedDownloads(data.failed_downloads || []);
|
||||
|
||||
// Update button states
|
||||
updateButtonStates(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update statistics display
|
||||
* @param {Object} stats - Statistics object
|
||||
* @param {Object} data - Full queue data
|
||||
*/
|
||||
function updateStatistics(stats, data) {
|
||||
const statistics = stats || {};
|
||||
|
||||
document.getElementById('total-items').textContent = statistics.total_items || 0;
|
||||
document.getElementById('pending-items').textContent = (data.pending_queue || []).length;
|
||||
document.getElementById('completed-items').textContent = statistics.completed_items || 0;
|
||||
document.getElementById('failed-items').textContent = statistics.failed_items || 0;
|
||||
|
||||
// Update section counts
|
||||
document.getElementById('queue-count').textContent = (data.pending_queue || []).length;
|
||||
document.getElementById('completed-count').textContent = statistics.completed_items || 0;
|
||||
document.getElementById('failed-count').textContent = statistics.failed_items || 0;
|
||||
|
||||
document.getElementById('current-speed').textContent = statistics.current_speed || '0 MB/s';
|
||||
document.getElementById('average-speed').textContent = statistics.average_speed || '0 MB/s';
|
||||
|
||||
// Format ETA
|
||||
const etaElement = document.getElementById('eta-time');
|
||||
if (statistics.eta) {
|
||||
const eta = new Date(statistics.eta);
|
||||
const now = new Date();
|
||||
const diffMs = eta - now;
|
||||
|
||||
if (diffMs > 0) {
|
||||
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
etaElement.textContent = hours + 'h ' + minutes + 'm';
|
||||
} else {
|
||||
etaElement.textContent = 'Calculating...';
|
||||
}
|
||||
} else {
|
||||
etaElement.textContent = '--:--';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render active downloads
|
||||
* @param {Array} downloads - Active downloads array
|
||||
*/
|
||||
function renderActiveDownloads(downloads) {
|
||||
const container = document.getElementById('active-downloads');
|
||||
|
||||
if (downloads.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="empty-state">' +
|
||||
'<i class="fas fa-pause-circle"></i>' +
|
||||
'<p>No active downloads</p>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = downloads.map(function(download) {
|
||||
return createActiveDownloadCard(download);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create active download card HTML
|
||||
* @param {Object} download - Download item
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function createActiveDownloadCard(download) {
|
||||
const progress = download.progress || {};
|
||||
const progressPercent = progress.percent || 0;
|
||||
const speed = progress.speed_mbps ? progress.speed_mbps.toFixed(1) + ' MB/s' : '0 MB/s';
|
||||
|
||||
const episodeNum = String(download.episode.episode).padStart(2, '0');
|
||||
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
|
||||
|
||||
return '<div class="download-card active" data-download-id="' + download.id + '">' +
|
||||
'<div class="download-header">' +
|
||||
'<div class="download-info">' +
|
||||
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
|
||||
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
|
||||
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="download-progress">' +
|
||||
'<div class="progress-bar">' +
|
||||
'<div class="progress-fill" style="width: ' + progressPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="progress-info">' +
|
||||
'<span>' + (progressPercent > 0 ? progressPercent.toFixed(1) + '%' : 'Starting...') + '</span>' +
|
||||
'<span class="download-speed">' + speed + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pending queue
|
||||
* @param {Array} queue - Pending queue array
|
||||
*/
|
||||
function renderPendingQueue(queue) {
|
||||
const container = document.getElementById('pending-queue');
|
||||
|
||||
if (queue.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="empty-state">' +
|
||||
'<i class="fas fa-list"></i>' +
|
||||
'<p>No items in queue</p>' +
|
||||
'<small>Add episodes from the main page to start downloading</small>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = queue.map(function(item, index) {
|
||||
return createPendingQueueCard(item, index);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pending queue card HTML
|
||||
* @param {Object} download - Download item
|
||||
* @param {number} index - Queue position
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function createPendingQueueCard(download, index) {
|
||||
const addedAt = new Date(download.added_at).toLocaleString();
|
||||
const episodeNum = String(download.episode.episode).padStart(2, '0');
|
||||
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
|
||||
|
||||
return '<div class="download-card pending" data-id="' + download.id + '" data-index="' + index + '">' +
|
||||
'<div class="queue-position">' + (index + 1) + '</div>' +
|
||||
'<div class="download-header">' +
|
||||
'<div class="download-info">' +
|
||||
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
|
||||
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
|
||||
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
|
||||
'<small>Added: ' + addedAt + '</small>' +
|
||||
'</div>' +
|
||||
'<div class="download-actions">' +
|
||||
'<button class="btn btn-small btn-secondary" onclick="AniWorld.QueueApp.removeFromQueue(\'' + download.id + '\')">' +
|
||||
'<i class="fas fa-trash"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render completed downloads
|
||||
* @param {Array} downloads - Completed downloads array
|
||||
*/
|
||||
function renderCompletedDownloads(downloads) {
|
||||
const container = document.getElementById('completed-downloads');
|
||||
|
||||
if (downloads.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="empty-state">' +
|
||||
'<i class="fas fa-check-circle"></i>' +
|
||||
'<p>No completed downloads</p>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = downloads.map(function(download) {
|
||||
return createCompletedDownloadCard(download);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create completed download card HTML
|
||||
* @param {Object} download - Download item
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function createCompletedDownloadCard(download) {
|
||||
const completedAt = new Date(download.completed_at).toLocaleString();
|
||||
const duration = AniWorld.UI.calculateDuration(download.started_at, download.completed_at);
|
||||
const episodeNum = String(download.episode.episode).padStart(2, '0');
|
||||
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
|
||||
|
||||
return '<div class="download-card completed">' +
|
||||
'<div class="download-header">' +
|
||||
'<div class="download-info">' +
|
||||
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
|
||||
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
|
||||
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
|
||||
'<small>Completed: ' + completedAt + ' (' + duration + ')</small>' +
|
||||
'</div>' +
|
||||
'<div class="download-status">' +
|
||||
'<i class="fas fa-check-circle text-success"></i>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render failed downloads
|
||||
* @param {Array} downloads - Failed downloads array
|
||||
*/
|
||||
function renderFailedDownloads(downloads) {
|
||||
const container = document.getElementById('failed-downloads');
|
||||
|
||||
if (downloads.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="empty-state">' +
|
||||
'<i class="fas fa-check-circle text-success"></i>' +
|
||||
'<p>No failed downloads</p>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = downloads.map(function(download) {
|
||||
return createFailedDownloadCard(download);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create failed download card HTML
|
||||
* @param {Object} download - Download item
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function createFailedDownloadCard(download) {
|
||||
const failedAt = new Date(download.completed_at).toLocaleString();
|
||||
const retryCount = download.retry_count || 0;
|
||||
const episodeNum = String(download.episode.episode).padStart(2, '0');
|
||||
const episodeTitle = download.episode.title || 'Episode ' + download.episode.episode;
|
||||
|
||||
return '<div class="download-card failed" data-id="' + download.id + '">' +
|
||||
'<div class="download-header">' +
|
||||
'<div class="download-info">' +
|
||||
'<h4>' + AniWorld.UI.escapeHtml(download.serie_name) + '</h4>' +
|
||||
'<p>' + AniWorld.UI.escapeHtml(download.episode.season) + 'x' + episodeNum + ' - ' +
|
||||
AniWorld.UI.escapeHtml(episodeTitle) + '</p>' +
|
||||
'<small>Failed: ' + failedAt + (retryCount > 0 ? ' (Retry ' + retryCount + ')' : '') + '</small>' +
|
||||
(download.error ? '<small class="error-message">' + AniWorld.UI.escapeHtml(download.error) + '</small>' : '') +
|
||||
'</div>' +
|
||||
'<div class="download-actions">' +
|
||||
'<button class="btn btn-small btn-warning" onclick="AniWorld.QueueApp.retryDownload(\'' + download.id + '\')">' +
|
||||
'<i class="fas fa-redo"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn btn-small btn-secondary" onclick="AniWorld.QueueApp.removeFromQueue(\'' + download.id + '\')">' +
|
||||
'<i class="fas fa-trash"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button states based on queue data
|
||||
* @param {Object} data - Queue data
|
||||
*/
|
||||
function updateButtonStates(data) {
|
||||
const hasActive = (data.active_downloads || []).length > 0;
|
||||
const hasPending = (data.pending_queue || []).length > 0;
|
||||
const hasFailed = (data.failed_downloads || []).length > 0;
|
||||
const hasCompleted = (data.completed_downloads || []).length > 0;
|
||||
|
||||
console.log('Button states update:', {
|
||||
hasPending: hasPending,
|
||||
pendingCount: (data.pending_queue || []).length,
|
||||
hasActive: hasActive,
|
||||
hasFailed: hasFailed,
|
||||
hasCompleted: hasCompleted
|
||||
});
|
||||
|
||||
// Enable start button only if there are pending items and no active downloads
|
||||
document.getElementById('start-queue-btn').disabled = !hasPending || hasActive;
|
||||
|
||||
// Show/hide start/stop buttons based on whether downloads are active
|
||||
if (hasActive) {
|
||||
document.getElementById('start-queue-btn').style.display = 'none';
|
||||
document.getElementById('stop-queue-btn').style.display = 'inline-flex';
|
||||
document.getElementById('stop-queue-btn').disabled = false;
|
||||
} else {
|
||||
document.getElementById('stop-queue-btn').style.display = 'none';
|
||||
document.getElementById('start-queue-btn').style.display = 'inline-flex';
|
||||
}
|
||||
|
||||
document.getElementById('retry-all-btn').disabled = !hasFailed;
|
||||
document.getElementById('clear-completed-btn').disabled = !hasCompleted;
|
||||
document.getElementById('clear-failed-btn').disabled = !hasFailed;
|
||||
|
||||
// Update clear pending button if it exists
|
||||
const clearPendingBtn = document.getElementById('clear-pending-btn');
|
||||
if (clearPendingBtn) {
|
||||
clearPendingBtn.disabled = !hasPending;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
updateQueueDisplay: updateQueueDisplay,
|
||||
updateStatistics: updateStatistics,
|
||||
renderActiveDownloads: renderActiveDownloads,
|
||||
renderPendingQueue: renderPendingQueue,
|
||||
renderCompletedDownloads: renderCompletedDownloads,
|
||||
renderFailedDownloads: renderFailedDownloads,
|
||||
updateButtonStates: updateButtonStates
|
||||
};
|
||||
})();
|
||||
161
src/server/web/static/js/queue/queue-socket-handler.js
Normal file
161
src/server/web/static/js/queue/queue-socket-handler.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* AniWorld - Queue Socket Handler Module
|
||||
*
|
||||
* Handles WebSocket events specific to the queue page.
|
||||
*
|
||||
* Dependencies: constants.js, websocket-client.js, ui-utils.js, queue-renderer.js, progress-handler.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.QueueSocketHandler = (function() {
|
||||
'use strict';
|
||||
|
||||
const WS_EVENTS = AniWorld.Constants.WS_EVENTS;
|
||||
|
||||
// Reference to queue app for data reloading
|
||||
let queueApp = null;
|
||||
|
||||
/**
|
||||
* Initialize socket handler
|
||||
* @param {Object} app - Reference to queue app
|
||||
*/
|
||||
function init(app) {
|
||||
queueApp = app;
|
||||
setupSocketHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up WebSocket event handlers
|
||||
*/
|
||||
function setupSocketHandlers() {
|
||||
const socket = AniWorld.WebSocketClient.getSocket();
|
||||
if (!socket) {
|
||||
console.warn('Socket not available for handler setup');
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection events
|
||||
socket.on('connect', function() {
|
||||
AniWorld.UI.showToast('Connected to server', 'success');
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
AniWorld.UI.showToast('Disconnected from server', 'warning');
|
||||
});
|
||||
|
||||
// Queue update events - handle both old and new message types
|
||||
socket.on('queue_updated', function(data) {
|
||||
AniWorld.QueueRenderer.updateQueueDisplay(data);
|
||||
});
|
||||
|
||||
socket.on('queue_status', function(data) {
|
||||
// New backend sends queue_status messages with nested structure
|
||||
if (data.status && data.statistics) {
|
||||
const queueData = {
|
||||
...data.status,
|
||||
statistics: data.statistics
|
||||
};
|
||||
AniWorld.QueueRenderer.updateQueueDisplay(queueData);
|
||||
} else if (data.queue_status) {
|
||||
AniWorld.QueueRenderer.updateQueueDisplay(data.queue_status);
|
||||
} else {
|
||||
AniWorld.QueueRenderer.updateQueueDisplay(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Download started events
|
||||
socket.on('download_started', function() {
|
||||
AniWorld.UI.showToast('Download queue started', 'success');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
});
|
||||
|
||||
socket.on('queue_started', function() {
|
||||
AniWorld.UI.showToast('Download queue started', 'success');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
});
|
||||
|
||||
// Download progress
|
||||
socket.on('download_progress', function(data) {
|
||||
console.log('Received download progress:', data);
|
||||
const success = AniWorld.ProgressHandler.updateDownloadProgress(data);
|
||||
if (!success && queueApp) {
|
||||
// Card not found, reload queue
|
||||
queueApp.loadQueueData();
|
||||
}
|
||||
});
|
||||
|
||||
// Download complete events
|
||||
const handleDownloadComplete = function(data) {
|
||||
const serieName = data.serie_name || data.serie || 'Unknown';
|
||||
const episode = data.episode || '';
|
||||
AniWorld.UI.showToast('Completed: ' + serieName + (episode ? ' - Episode ' + episode : ''), 'success');
|
||||
|
||||
// Clear pending progress updates
|
||||
const downloadId = data.item_id || data.download_id || data.id;
|
||||
if (downloadId) {
|
||||
AniWorld.ProgressHandler.clearPendingUpdate(downloadId);
|
||||
}
|
||||
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
};
|
||||
socket.on(WS_EVENTS.DOWNLOAD_COMPLETED, handleDownloadComplete);
|
||||
socket.on(WS_EVENTS.DOWNLOAD_COMPLETE, handleDownloadComplete);
|
||||
|
||||
// Download error events
|
||||
const handleDownloadError = function(data) {
|
||||
const message = data.error || data.message || 'Unknown error';
|
||||
AniWorld.UI.showToast('Download failed: ' + message, 'error');
|
||||
|
||||
// Clear pending progress updates
|
||||
const downloadId = data.item_id || data.download_id || data.id;
|
||||
if (downloadId) {
|
||||
AniWorld.ProgressHandler.clearPendingUpdate(downloadId);
|
||||
}
|
||||
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
};
|
||||
socket.on(WS_EVENTS.DOWNLOAD_ERROR, handleDownloadError);
|
||||
socket.on(WS_EVENTS.DOWNLOAD_FAILED, handleDownloadError);
|
||||
|
||||
// Queue completed events
|
||||
socket.on(WS_EVENTS.DOWNLOAD_QUEUE_COMPLETED, function() {
|
||||
AniWorld.UI.showToast('All downloads completed!', 'success');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.QUEUE_COMPLETED, function() {
|
||||
AniWorld.UI.showToast('All downloads completed!', 'success');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
});
|
||||
|
||||
// Download stop requested
|
||||
socket.on(WS_EVENTS.DOWNLOAD_STOP_REQUESTED, function() {
|
||||
AniWorld.UI.showToast('Stopping downloads...', 'info');
|
||||
});
|
||||
|
||||
// Queue stopped events
|
||||
const handleQueueStopped = function() {
|
||||
AniWorld.UI.showToast('Download queue stopped', 'success');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
};
|
||||
socket.on(WS_EVENTS.DOWNLOAD_STOPPED, handleQueueStopped);
|
||||
socket.on(WS_EVENTS.QUEUE_STOPPED, handleQueueStopped);
|
||||
|
||||
// Queue paused/resumed
|
||||
socket.on(WS_EVENTS.QUEUE_PAUSED, function() {
|
||||
AniWorld.UI.showToast('Queue paused', 'info');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.QUEUE_RESUMED, function() {
|
||||
AniWorld.UI.showToast('Queue resumed', 'success');
|
||||
if (queueApp) queueApp.loadQueueData();
|
||||
});
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user