diff --git a/src/server/app.py b/src/server/app.py index b694c66..9c6380e 100644 --- a/src/server/app.py +++ b/src/server/app.py @@ -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 diff --git a/src/server/application/services/bulk_service.py b/src/server/application/services/bulk_service.py index b0e4274..422bf32 100644 --- a/src/server/application/services/bulk_service.py +++ b/src/server/application/services/bulk_service.py @@ -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' ); diff --git a/src/server/web/static/css/styles.css b/src/server/web/static/css/styles.css index 700dd1c..a0efead 100644 --- a/src/server/web/static/css/styles.css +++ b/src/server/web/static/css/styles.css @@ -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 */ } } diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index e90f2ae..2f5d86b 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -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'; diff --git a/src/server/web/static/js/queue.js b/src/server/web/static/js/queue.js index d76f38e..4d84476 100644 --- a/src/server/web/static/js/queue.js +++ b/src/server/web/static/js/queue.js @@ -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 = `
@@ -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 `
@@ -238,7 +281,7 @@ class QueueManager { renderPendingQueue(queue) { const container = document.getElementById('pending-queue'); - + if (queue.length === 0) { container.innerHTML = `
@@ -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 `
${index + 1}
@@ -278,7 +321,7 @@ class QueueManager { renderCompletedDownloads(downloads) { const container = document.getElementById('completed-downloads'); - + if (downloads.length === 0) { container.innerHTML = `
@@ -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 `
@@ -314,7 +357,7 @@ class QueueManager { renderFailedDownloads(downloads) { const container = document.getElementById('failed-downloads'); - + if (downloads.length === 0) { container.innerHTML = `
@@ -331,7 +374,7 @@ class QueueManager { createFailedDownloadCard(download) { const failedAt = new Date(download.completed_at).toLocaleString(); const retryCount = download.retry_count || 0; - + return `
@@ -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 = `
@@ -533,9 +660,9 @@ class QueueManager {
`; - + 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(() => { diff --git a/src/server/web/templates/base/queue.html b/src/server/web/templates/base/queue.html index 4729ecc..b0e5ee4 100644 --- a/src/server/web/templates/base/queue.html +++ b/src/server/web/templates/base/queue.html @@ -1,5 +1,6 @@ + @@ -7,6 +8,7 @@ +
@@ -46,7 +48,7 @@
Total Items
- +
@@ -56,7 +58,7 @@
In Queue
- +
@@ -66,7 +68,7 @@
Completed
- +
@@ -77,7 +79,7 @@
- +
@@ -115,7 +117,7 @@
- +
@@ -132,6 +134,14 @@ Download Queue
+ +
- +
@@ -165,7 +175,7 @@
- +
@@ -192,7 +202,7 @@
- +
@@ -238,4 +248,5 @@ + \ No newline at end of file diff --git a/test_stop_functionality.py b/test_stop_functionality.py new file mode 100644 index 0000000..2f10c4d --- /dev/null +++ b/test_stop_functionality.py @@ -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() \ No newline at end of file