This commit is contained in:
2025-09-29 11:51:58 +02:00
parent f9102d7bcd
commit b2d77a099b
7 changed files with 539 additions and 66 deletions

View File

@@ -206,6 +206,7 @@ class AniWorldApp {
this.socket.on('download_started', (data) => {
this.isDownloading = true;
this.isPaused = false;
this.updateProcessStatus('download', true);
this.showDownloadQueue(data);
this.showStatus(`Starting download of ${data.total_series} series...`, true, true);
});
@@ -237,6 +238,21 @@ class AniWorldApp {
this.showToast(`${this.localization.getText('download-failed')}: ${data.message}`, 'error');
});
// Download queue status events
this.socket.on('download_queue_completed', () => {
this.updateProcessStatus('download', false);
this.showToast('All downloads completed!', 'success');
});
this.socket.on('download_stop_requested', () => {
this.showToast('Stopping downloads...', 'info');
});
this.socket.on('download_stopped', () => {
this.updateProcessStatus('download', false);
this.showToast('Downloads stopped', 'success');
});
// Download queue events
this.socket.on('download_queue_update', (data) => {
this.updateDownloadQueue(data);
@@ -476,7 +492,13 @@ class AniWorldApp {
}
async makeAuthenticatedRequest(url, options = {}) {
const response = await fetch(url, options);
// Ensure credentials are included for session-based authentication
const requestOptions = {
credentials: 'same-origin',
...options
};
const response = await fetch(url, requestOptions);
if (response.status === 401) {
window.location.href = '/login';

View File

@@ -7,7 +7,7 @@ class QueueManager {
this.socket = null;
this.refreshInterval = null;
this.isReordering = false;
this.init();
}
@@ -21,7 +21,7 @@ class QueueManager {
initSocket() {
this.socket = io();
this.socket.on('connect', () => {
console.log('Connected to server');
this.showToast('Connected to server', 'success');
@@ -40,6 +40,41 @@ class QueueManager {
this.socket.on('download_progress_update', (data) => {
this.updateDownloadProgress(data);
});
// Download queue events
this.socket.on('download_started', () => {
this.showToast('Download queue started', 'success');
this.loadQueueData(); // Refresh data
});
this.socket.on('download_progress', (data) => {
this.updateDownloadProgress(data);
});
this.socket.on('download_completed', (data) => {
this.showToast(`Completed: ${data.serie} - Episode ${data.episode}`, 'success');
this.loadQueueData(); // Refresh data
});
this.socket.on('download_error', (data) => {
const message = data.error || data.message || 'Unknown error';
this.showToast(`Download failed: ${message}`, 'error');
this.loadQueueData(); // Refresh data
});
this.socket.on('download_queue_completed', () => {
this.showToast('All downloads completed!', 'success');
this.loadQueueData(); // Refresh data
});
this.socket.on('download_stop_requested', () => {
this.showToast('Stopping downloads...', 'info');
});
this.socket.on('download_stopped', () => {
this.showToast('Download queue stopped', 'success');
this.loadQueueData(); // Refresh data
});
}
bindEvents() {
@@ -70,6 +105,14 @@ class QueueManager {
});
// Download controls
document.getElementById('start-queue-btn').addEventListener('click', () => {
this.startDownloadQueue();
});
document.getElementById('stop-queue-btn').addEventListener('click', () => {
this.stopDownloadQueue();
});
document.getElementById('pause-all-btn').addEventListener('click', () => {
this.pauseAllDownloads();
});
@@ -105,7 +148,7 @@ class QueueManager {
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const themeIcon = document.querySelector('#theme-toggle i');
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
}
@@ -127,10 +170,10 @@ class QueueManager {
try {
const response = await this.makeAuthenticatedRequest('/api/queue/status');
if (!response) return;
const data = await response.json();
this.updateQueueDisplay(data);
} catch (error) {
console.error('Error loading queue data:', error);
}
@@ -139,19 +182,19 @@ class QueueManager {
updateQueueDisplay(data) {
// Update statistics
this.updateStatistics(data.statistics, data);
// Update active downloads
this.renderActiveDownloads(data.active_downloads || []);
// Update pending queue
this.renderPendingQueue(data.pending_queue || []);
// Update completed downloads
this.renderCompletedDownloads(data.completed_downloads || []);
// Update failed downloads
this.renderFailedDownloads(data.failed_downloads || []);
// Update button states
this.updateButtonStates(data);
}
@@ -161,17 +204,17 @@ class QueueManager {
document.getElementById('pending-items').textContent = (data.pending_queue || []).length;
document.getElementById('completed-items').textContent = stats.completed_items || 0;
document.getElementById('failed-items').textContent = stats.failed_items || 0;
document.getElementById('current-speed').textContent = stats.current_speed || '0 MB/s';
document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s';
// Format ETA
const etaElement = document.getElementById('eta-time');
if (stats.eta) {
const eta = new Date(stats.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));
@@ -186,7 +229,7 @@ class QueueManager {
renderActiveDownloads(downloads) {
const container = document.getElementById('active-downloads');
if (downloads.length === 0) {
container.innerHTML = `
<div class="empty-state">
@@ -206,7 +249,7 @@ class QueueManager {
const speed = progress.speed_mbps ? `${progress.speed_mbps.toFixed(1)} MB/s` : '0 MB/s';
const downloaded = progress.downloaded_mb ? `${progress.downloaded_mb.toFixed(1)} MB` : '0 MB';
const total = progress.total_mb ? `${progress.total_mb.toFixed(1)} MB` : 'Unknown';
return `
<div class="download-card active">
<div class="download-header">
@@ -238,7 +281,7 @@ class QueueManager {
renderPendingQueue(queue) {
const container = document.getElementById('pending-queue');
if (queue.length === 0) {
container.innerHTML = `
<div class="empty-state">
@@ -255,7 +298,7 @@ class QueueManager {
createPendingQueueCard(download, index) {
const addedAt = new Date(download.added_at).toLocaleString();
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
return `
<div class="download-card pending ${priorityClass}" data-id="${download.id}">
<div class="queue-position">${index + 1}</div>
@@ -278,7 +321,7 @@ class QueueManager {
renderCompletedDownloads(downloads) {
const container = document.getElementById('completed-downloads');
if (downloads.length === 0) {
container.innerHTML = `
<div class="empty-state">
@@ -295,7 +338,7 @@ class QueueManager {
createCompletedDownloadCard(download) {
const completedAt = new Date(download.completed_at).toLocaleString();
const duration = this.calculateDuration(download.started_at, download.completed_at);
return `
<div class="download-card completed">
<div class="download-header">
@@ -314,7 +357,7 @@ class QueueManager {
renderFailedDownloads(downloads) {
const container = document.getElementById('failed-downloads');
if (downloads.length === 0) {
container.innerHTML = `
<div class="empty-state">
@@ -331,7 +374,7 @@ class QueueManager {
createFailedDownloadCard(download) {
const failedAt = new Date(download.completed_at).toLocaleString();
const retryCount = download.retry_count || 0;
return `
<div class="download-card failed">
<div class="download-header">
@@ -358,7 +401,20 @@ class QueueManager {
const hasActive = (data.active_downloads || []).length > 0;
const hasPending = (data.pending_queue || []).length > 0;
const hasFailed = (data.failed_downloads || []).length > 0;
// 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('pause-all-btn').disabled = !hasActive;
document.getElementById('clear-queue-btn').disabled = !hasPending;
document.getElementById('reorder-queue-btn').disabled = !hasPending || (data.pending_queue || []).length < 2;
@@ -371,33 +427,33 @@ class QueueManager {
completed: 'Clear Completed Downloads',
failed: 'Clear Failed Downloads'
};
const messages = {
pending: 'Are you sure you want to clear all pending downloads from the queue?',
completed: 'Are you sure you want to clear all completed downloads?',
failed: 'Are you sure you want to clear all failed downloads?'
};
const confirmed = await this.showConfirmModal(titles[type], messages[type]);
if (!confirmed) return;
try {
const response = await this.makeAuthenticatedRequest('/api/queue/clear', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type })
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast(data.message, 'success');
this.loadQueueData();
} else {
this.showToast(data.message, 'error');
}
} catch (error) {
console.error('Error clearing queue:', error);
this.showToast('Failed to clear queue', 'error');
@@ -411,17 +467,17 @@ class QueueManager {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: downloadId })
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download added back to queue', 'success');
this.loadQueueData();
} else {
this.showToast(data.message, 'error');
}
} catch (error) {
console.error('Error retrying download:', error);
this.showToast('Failed to retry download', 'error');
@@ -431,10 +487,10 @@ class QueueManager {
async retryAllFailed() {
const confirmed = await this.showConfirmModal('Retry All Failed Downloads', 'Are you sure you want to retry all failed downloads?');
if (!confirmed) return;
// Get all failed downloads and retry them individually
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
for (const card of failedCards) {
const downloadId = card.dataset.id;
if (downloadId) {
@@ -450,17 +506,17 @@ class QueueManager {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: downloadId })
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download removed from queue', 'success');
this.loadQueueData();
} else {
this.showToast(data.message, 'error');
}
} catch (error) {
console.error('Error removing from queue:', error);
this.showToast('Failed to remove from queue', 'error');
@@ -471,21 +527,92 @@ class QueueManager {
const start = new Date(startTime);
const end = new Date(endTime);
const diffMs = end - start;
const minutes = Math.floor(diffMs / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
return `${minutes}m ${seconds}s`;
}
async startDownloadQueue() {
try {
const response = await this.makeAuthenticatedRequest('/api/queue/start', {
method: 'POST'
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download queue started', '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;
} else {
this.showToast(`Failed to start queue: ${data.message}`, 'error');
}
} catch (error) {
console.error('Error starting download queue:', error);
this.showToast('Failed to start download queue', 'error');
}
}
async stopDownloadQueue() {
try {
const response = await this.makeAuthenticatedRequest('/api/queue/stop', {
method: 'POST'
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download queue 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;
} else {
this.showToast(`Failed to stop queue: ${data.message}`, 'error');
}
} catch (error) {
console.error('Error stopping download queue:', error);
this.showToast('Failed to stop download queue', 'error');
}
}
pauseAllDownloads() {
// TODO: Implement pause functionality
this.showToast('Pause functionality not yet implemented', 'info');
}
resumeAllDownloads() {
// TODO: Implement resume functionality
this.showToast('Resume functionality not yet implemented', 'info');
}
toggleReorderMode() {
// TODO: Implement reorder functionality
this.showToast('Reorder functionality not yet implemented', 'info');
}
async makeAuthenticatedRequest(url, options = {}) {
const response = await fetch(url, options);
// Ensure credentials are included for session-based authentication
const requestOptions = {
credentials: 'same-origin',
...options
};
const response = await fetch(url, requestOptions);
if (response.status === 401) {
window.location.href = '/login';
return null;
}
return response;
}
@@ -494,23 +621,23 @@ class QueueManager {
document.getElementById('confirm-title').textContent = title;
document.getElementById('confirm-message').textContent = message;
document.getElementById('confirm-modal').classList.remove('hidden');
const handleConfirm = () => {
cleanup();
resolve(true);
};
const handleCancel = () => {
cleanup();
resolve(false);
};
const cleanup = () => {
document.getElementById('confirm-ok').removeEventListener('click', handleConfirm);
document.getElementById('confirm-cancel').removeEventListener('click', handleCancel);
this.hideConfirmModal();
};
document.getElementById('confirm-ok').addEventListener('click', handleConfirm);
document.getElementById('confirm-cancel').addEventListener('click', handleCancel);
});
@@ -523,7 +650,7 @@ class QueueManager {
showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
@@ -533,9 +660,9 @@ class QueueManager {
</button>
</div>
`;
container.appendChild(toast);
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
@@ -553,7 +680,7 @@ class QueueManager {
try {
const response = await fetch('/api/auth/logout', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
this.showToast('Logged out successfully', 'success');
setTimeout(() => {