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

View File

@ -267,6 +267,7 @@ series_app = None
is_scanning = False
is_downloading = False
is_paused = False
should_stop_downloads = False
download_thread = None
download_progress = {}
download_queue = []
@ -873,11 +874,120 @@ def rescan_series():
'message': 'Rescan started'
})
# Basic download endpoint - simplified for now
# Download endpoint - adds items to queue
@app.route('/api/download', methods=['POST'])
@optional_auth
def download_series():
"""Download selected series."""
"""Add selected series to download queue."""
try:
data = request.get_json()
if not data or 'folders' not in data:
return jsonify({
'status': 'error',
'message': 'Folders list is required'
}), 400
folders = data['folders']
if not folders:
return jsonify({
'status': 'error',
'message': 'No series selected'
}), 400
# Import the queue functions
from application.services.queue_service import add_to_download_queue
added_count = 0
for folder in folders:
try:
# Find the serie in our list
serie = None
if series_app and series_app.List:
for s in series_app.List.GetList():
if s.folder == folder:
serie = s
break
if serie:
# Check if this serie has missing episodes (non-empty episodeDict)
if serie.episodeDict:
# Create download entries for each season/episode combination
for season, episodes in serie.episodeDict.items():
for episode in episodes:
episode_info = {
'folder': folder,
'season': season,
'episode_number': episode,
'title': f'S{season:02d}E{episode:02d}',
'url': '', # Will be populated during actual download
'serie_name': serie.name or folder
}
add_to_download_queue(
serie_name=serie.name or folder,
episode_info=episode_info,
priority='normal'
)
added_count += 1
else:
# No missing episodes, add a placeholder entry indicating series is complete
episode_info = {
'folder': folder,
'season': None,
'episode_number': 'Complete',
'title': 'No missing episodes',
'url': '',
'serie_name': serie.name or folder
}
add_to_download_queue(
serie_name=serie.name or folder,
episode_info=episode_info,
priority='normal'
)
added_count += 1
else:
# Serie not found, add with folder name only
episode_info = {
'folder': folder,
'episode_number': 'Unknown',
'title': 'Serie Check Required',
'url': '',
'serie_name': folder
}
add_to_download_queue(
serie_name=folder,
episode_info=episode_info,
priority='normal'
)
added_count += 1
except Exception as e:
print(f"Error processing folder {folder}: {e}")
continue
if added_count > 0:
return jsonify({
'status': 'success',
'message': f'Added {added_count} items to download queue'
})
else:
return jsonify({
'status': 'error',
'message': 'No items could be added to the queue'
}), 400
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Failed to add to queue: {str(e)}'
}), 500
@app.route('/api/queue/start', methods=['POST'])
@optional_auth
def start_download_queue():
"""Start processing the download queue."""
global is_downloading
# Check if download is already running using process lock
@ -888,9 +998,140 @@ def download_series():
'is_running': True
}), 409
def download_thread():
global is_downloading, should_stop_downloads
should_stop_downloads = False # Reset stop flag when starting
try:
# Use process lock to prevent duplicate downloads
@with_process_lock(DOWNLOAD_LOCK, timeout_minutes=720) # 12 hours max
def perform_downloads():
global is_downloading
is_downloading = True
try:
from application.services.queue_service import start_next_download, move_download_to_completed, update_download_progress
# Emit download started
socketio.emit('download_started')
# Process queue items
while True:
# Check for stop signal
global should_stop_downloads
if should_stop_downloads:
should_stop_downloads = False # Reset the flag
break
# Start next download
current_download = start_next_download()
if not current_download:
break # No more items in queue
try:
socketio.emit('download_progress', {
'id': current_download['id'],
'serie': current_download['serie_name'],
'episode': current_download['episode']['episode_number'],
'status': 'downloading'
})
# Simulate download process (replace with actual download logic)
import time
for i in range(0, 101, 10):
# Check for stop signal during download
if should_stop_downloads:
move_download_to_completed(current_download['id'], success=False, error='Download stopped by user')
socketio.emit('download_stopped', {
'message': 'Download queue stopped by user'
})
should_stop_downloads = False
raise Exception('Download stopped by user')
update_download_progress(current_download['id'], {
'percent': i,
'speed_mbps': 2.5,
'eta_seconds': (100 - i) * 2
})
socketio.emit('download_progress', {
'id': current_download['id'],
'serie': current_download['serie_name'],
'episode': current_download['episode']['episode_number'],
'progress': i
})
time.sleep(0.5) # Simulate download time
# Mark as completed
move_download_to_completed(current_download['id'], success=True)
socketio.emit('download_completed', {
'id': current_download['id'],
'serie': current_download['serie_name'],
'episode': current_download['episode']['episode_number']
})
except Exception as e:
# Mark as failed
move_download_to_completed(current_download['id'], success=False, error=str(e))
socketio.emit('download_error', {
'id': current_download['id'],
'serie': current_download['serie_name'],
'episode': current_download['episode']['episode_number'],
'error': str(e)
})
# Emit download queue completed
socketio.emit('download_queue_completed')
except Exception as e:
socketio.emit('download_error', {'message': str(e)})
raise
finally:
is_downloading = False
perform_downloads(_locked_by='web_interface')
except ProcessLockError:
socketio.emit('download_error', {'message': 'Download is already running'})
except Exception as e:
socketio.emit('download_error', {'message': str(e)})
# Start download in background thread
threading.Thread(target=download_thread, daemon=True).start()
return jsonify({
'status': 'success',
'message': 'Download functionality will be implemented with queue system'
'message': 'Download queue processing started'
})
@app.route('/api/queue/stop', methods=['POST'])
@optional_auth
def stop_download_queue():
"""Stop processing the download queue."""
global is_downloading, should_stop_downloads
# Check if any download is currently running
if not is_downloading and not is_process_running(DOWNLOAD_LOCK):
return jsonify({
'status': 'error',
'message': 'No download is currently running'
}), 400
# Set stop signal for graceful shutdown
should_stop_downloads = True
# Don't forcefully set is_downloading to False here, let the download thread handle it
# This prevents race conditions where the thread might still be running
# Emit stop signal to clients immediately
socketio.emit('download_stop_requested')
return jsonify({
'status': 'success',
'message': 'Download stop requested. Downloads will stop gracefully.'
})
# WebSocket events for real-time updates

View File

@ -414,7 +414,7 @@ class BulkOperationsManager {
const confirmed = await this.confirmOperation(
'Bulk Delete',
`Permanently delete ${this.selectedItems.size} selected series?\\n\\nThis action cannot be undone`,
`Permanently delete ${this.selectedItems.size} selected series?\\n\\nThis action cannot be undone!`,
'danger'
);

View File

@ -1437,17 +1437,20 @@ body {
}
.status-indicator i {
font-size: 24px; /* 2x bigger: 12px -> 24px */
font-size: 24px;
/* 2x bigger: 12px -> 24px */
transition: all var(--animation-duration-normal) var(--animation-easing-standard);
}
/* Rescan icon specific styling */
#rescan-status i {
color: var(--color-text-disabled); /* Gray when idle */
color: var(--color-text-disabled);
/* Gray when idle */
}
#rescan-status.running i {
color: #22c55e; /* Green when running */
color: #22c55e;
/* Green when running */
animation: iconPulse 2s infinite;
}
@ -1474,6 +1477,7 @@ body {
}
@keyframes pulse {
0%,
100% {
opacity: 1;
@ -1487,6 +1491,7 @@ body {
}
@keyframes iconPulse {
0%,
100% {
opacity: 1;
@ -1514,7 +1519,8 @@ body {
}
.status-indicator i {
font-size: 20px; /* Maintain 2x scale for mobile: was 14px -> 20px */
font-size: 20px;
/* Maintain 2x scale for mobile: was 14px -> 20px */
}
}

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(() => {

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -7,6 +8,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="app-container">
<!-- Header -->
@ -46,7 +48,7 @@
<div class="stat-label">Total Items</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock text-warning"></i>
@ -56,7 +58,7 @@
<div class="stat-label">In Queue</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-check-circle text-success"></i>
@ -66,7 +68,7 @@
<div class="stat-label">Completed</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-exclamation-triangle text-error"></i>
@ -77,7 +79,7 @@
</div>
</div>
</div>
<!-- Speed and ETA -->
<div class="speed-eta-section">
<div class="speed-info">
@ -115,7 +117,7 @@
</button>
</div>
</div>
<div class="active-downloads-list" id="active-downloads">
<div class="empty-state">
<i class="fas fa-pause-circle"></i>
@ -132,6 +134,14 @@
Download Queue
</h2>
<div class="section-actions">
<button id="start-queue-btn" class="btn btn-primary" disabled>
<i class="fas fa-play"></i>
Start Downloads
</button>
<button id="stop-queue-btn" class="btn btn-secondary" disabled style="display: none;">
<i class="fas fa-stop"></i>
Stop Downloads
</button>
<button id="clear-queue-btn" class="btn btn-secondary" disabled>
<i class="fas fa-trash"></i>
Clear Queue
@ -142,7 +152,7 @@
</button>
</div>
</div>
<div class="pending-queue-list" id="pending-queue">
<div class="empty-state">
<i class="fas fa-list"></i>
@ -165,7 +175,7 @@
</button>
</div>
</div>
<div class="completed-downloads-list" id="completed-downloads">
<div class="empty-state">
<i class="fas fa-check-circle"></i>
@ -192,7 +202,7 @@
</button>
</div>
</div>
<div class="failed-downloads-list" id="failed-downloads">
<div class="empty-state">
<i class="fas fa-check-circle text-success"></i>
@ -238,4 +248,5 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="{{ url_for('static', filename='js/queue.js') }}"></script>
</body>
</html>

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""
Simple test script to verify the stop download functionality works properly.
This simulates the browser behavior without authentication requirements.
"""
import requests
import time
import json
def test_stop_download_functionality():
"""Test the stop download functionality."""
base_url = "http://127.0.0.1:5000"
print("Testing Stop Download Functionality")
print("=" * 50)
# Test 1: Try to stop when no downloads are running
print("\n1. Testing stop when no downloads are running...")
try:
response = requests.post(f"{base_url}/api/queue/stop", timeout=5)
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
if response.status_code == 400:
print("✓ Correctly returns error when no downloads are running")
elif response.status_code == 401:
print("⚠ Authentication required - this is expected")
else:
print(f"✗ Unexpected response: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"✗ Connection error: {e}")
return False
# Test 2: Check queue status endpoint
print("\n2. Testing queue status endpoint...")
try:
response = requests.get(f"{base_url}/api/queue/status", timeout=5)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
print(f"Response: {response.json()}")
print("✓ Queue status endpoint working")
elif response.status_code == 401:
print("⚠ Authentication required for queue status")
else:
print(f"✗ Unexpected status code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"✗ Connection error: {e}")
# Test 3: Check if the server is responding
print("\n3. Testing server health...")
try:
response = requests.get(f"{base_url}/", timeout=5)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
print("✓ Server is responding")
else:
print(f"⚠ Server returned: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"✗ Server connection error: {e}")
return True
if __name__ == "__main__":
test_stop_download_functionality()