backup
This commit is contained in:
parent
f9102d7bcd
commit
b2d77a099b
@ -267,6 +267,7 @@ series_app = None
|
|||||||
is_scanning = False
|
is_scanning = False
|
||||||
is_downloading = False
|
is_downloading = False
|
||||||
is_paused = False
|
is_paused = False
|
||||||
|
should_stop_downloads = False
|
||||||
download_thread = None
|
download_thread = None
|
||||||
download_progress = {}
|
download_progress = {}
|
||||||
download_queue = []
|
download_queue = []
|
||||||
@ -873,11 +874,120 @@ def rescan_series():
|
|||||||
'message': 'Rescan started'
|
'message': 'Rescan started'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Basic download endpoint - simplified for now
|
# Download endpoint - adds items to queue
|
||||||
@app.route('/api/download', methods=['POST'])
|
@app.route('/api/download', methods=['POST'])
|
||||||
@optional_auth
|
@optional_auth
|
||||||
def download_series():
|
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
|
global is_downloading
|
||||||
|
|
||||||
# Check if download is already running using process lock
|
# Check if download is already running using process lock
|
||||||
@ -888,9 +998,140 @@ def download_series():
|
|||||||
'is_running': True
|
'is_running': True
|
||||||
}), 409
|
}), 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({
|
return jsonify({
|
||||||
'status': 'success',
|
'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
|
# WebSocket events for real-time updates
|
||||||
|
|||||||
@ -414,7 +414,7 @@ class BulkOperationsManager {
|
|||||||
|
|
||||||
const confirmed = await this.confirmOperation(
|
const confirmed = await this.confirmOperation(
|
||||||
'Bulk Delete',
|
'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'
|
'danger'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1437,17 +1437,20 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator i {
|
.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);
|
transition: all var(--animation-duration-normal) var(--animation-easing-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rescan icon specific styling */
|
/* Rescan icon specific styling */
|
||||||
#rescan-status i {
|
#rescan-status i {
|
||||||
color: var(--color-text-disabled); /* Gray when idle */
|
color: var(--color-text-disabled);
|
||||||
|
/* Gray when idle */
|
||||||
}
|
}
|
||||||
|
|
||||||
#rescan-status.running i {
|
#rescan-status.running i {
|
||||||
color: #22c55e; /* Green when running */
|
color: #22c55e;
|
||||||
|
/* Green when running */
|
||||||
animation: iconPulse 2s infinite;
|
animation: iconPulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1474,6 +1477,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -1487,6 +1491,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes iconPulse {
|
@keyframes iconPulse {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -1514,7 +1519,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator i {
|
.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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -206,6 +206,7 @@ class AniWorldApp {
|
|||||||
this.socket.on('download_started', (data) => {
|
this.socket.on('download_started', (data) => {
|
||||||
this.isDownloading = true;
|
this.isDownloading = true;
|
||||||
this.isPaused = false;
|
this.isPaused = false;
|
||||||
|
this.updateProcessStatus('download', true);
|
||||||
this.showDownloadQueue(data);
|
this.showDownloadQueue(data);
|
||||||
this.showStatus(`Starting download of ${data.total_series} series...`, true, true);
|
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');
|
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
|
// Download queue events
|
||||||
this.socket.on('download_queue_update', (data) => {
|
this.socket.on('download_queue_update', (data) => {
|
||||||
this.updateDownloadQueue(data);
|
this.updateDownloadQueue(data);
|
||||||
@ -476,7 +492,13 @@ class AniWorldApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async makeAuthenticatedRequest(url, options = {}) {
|
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) {
|
if (response.status === 401) {
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
|
|||||||
@ -7,7 +7,7 @@ class QueueManager {
|
|||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.refreshInterval = null;
|
this.refreshInterval = null;
|
||||||
this.isReordering = false;
|
this.isReordering = false;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class QueueManager {
|
|||||||
|
|
||||||
initSocket() {
|
initSocket() {
|
||||||
this.socket = io();
|
this.socket = io();
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
console.log('Connected to server');
|
console.log('Connected to server');
|
||||||
this.showToast('Connected to server', 'success');
|
this.showToast('Connected to server', 'success');
|
||||||
@ -40,6 +40,41 @@ class QueueManager {
|
|||||||
this.socket.on('download_progress_update', (data) => {
|
this.socket.on('download_progress_update', (data) => {
|
||||||
this.updateDownloadProgress(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() {
|
bindEvents() {
|
||||||
@ -70,6 +105,14 @@ class QueueManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Download controls
|
// 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', () => {
|
document.getElementById('pause-all-btn').addEventListener('click', () => {
|
||||||
this.pauseAllDownloads();
|
this.pauseAllDownloads();
|
||||||
});
|
});
|
||||||
@ -105,7 +148,7 @@ class QueueManager {
|
|||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
|
|
||||||
const themeIcon = document.querySelector('#theme-toggle i');
|
const themeIcon = document.querySelector('#theme-toggle i');
|
||||||
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
|
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
|
||||||
}
|
}
|
||||||
@ -127,10 +170,10 @@ class QueueManager {
|
|||||||
try {
|
try {
|
||||||
const response = await this.makeAuthenticatedRequest('/api/queue/status');
|
const response = await this.makeAuthenticatedRequest('/api/queue/status');
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.updateQueueDisplay(data);
|
this.updateQueueDisplay(data);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading queue data:', error);
|
console.error('Error loading queue data:', error);
|
||||||
}
|
}
|
||||||
@ -139,19 +182,19 @@ class QueueManager {
|
|||||||
updateQueueDisplay(data) {
|
updateQueueDisplay(data) {
|
||||||
// Update statistics
|
// Update statistics
|
||||||
this.updateStatistics(data.statistics, data);
|
this.updateStatistics(data.statistics, data);
|
||||||
|
|
||||||
// Update active downloads
|
// Update active downloads
|
||||||
this.renderActiveDownloads(data.active_downloads || []);
|
this.renderActiveDownloads(data.active_downloads || []);
|
||||||
|
|
||||||
// Update pending queue
|
// Update pending queue
|
||||||
this.renderPendingQueue(data.pending_queue || []);
|
this.renderPendingQueue(data.pending_queue || []);
|
||||||
|
|
||||||
// Update completed downloads
|
// Update completed downloads
|
||||||
this.renderCompletedDownloads(data.completed_downloads || []);
|
this.renderCompletedDownloads(data.completed_downloads || []);
|
||||||
|
|
||||||
// Update failed downloads
|
// Update failed downloads
|
||||||
this.renderFailedDownloads(data.failed_downloads || []);
|
this.renderFailedDownloads(data.failed_downloads || []);
|
||||||
|
|
||||||
// Update button states
|
// Update button states
|
||||||
this.updateButtonStates(data);
|
this.updateButtonStates(data);
|
||||||
}
|
}
|
||||||
@ -161,17 +204,17 @@ class QueueManager {
|
|||||||
document.getElementById('pending-items').textContent = (data.pending_queue || []).length;
|
document.getElementById('pending-items').textContent = (data.pending_queue || []).length;
|
||||||
document.getElementById('completed-items').textContent = stats.completed_items || 0;
|
document.getElementById('completed-items').textContent = stats.completed_items || 0;
|
||||||
document.getElementById('failed-items').textContent = stats.failed_items || 0;
|
document.getElementById('failed-items').textContent = stats.failed_items || 0;
|
||||||
|
|
||||||
document.getElementById('current-speed').textContent = stats.current_speed || '0 MB/s';
|
document.getElementById('current-speed').textContent = stats.current_speed || '0 MB/s';
|
||||||
document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s';
|
document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s';
|
||||||
|
|
||||||
// Format ETA
|
// Format ETA
|
||||||
const etaElement = document.getElementById('eta-time');
|
const etaElement = document.getElementById('eta-time');
|
||||||
if (stats.eta) {
|
if (stats.eta) {
|
||||||
const eta = new Date(stats.eta);
|
const eta = new Date(stats.eta);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMs = eta - now;
|
const diffMs = eta - now;
|
||||||
|
|
||||||
if (diffMs > 0) {
|
if (diffMs > 0) {
|
||||||
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
@ -186,7 +229,7 @@ class QueueManager {
|
|||||||
|
|
||||||
renderActiveDownloads(downloads) {
|
renderActiveDownloads(downloads) {
|
||||||
const container = document.getElementById('active-downloads');
|
const container = document.getElementById('active-downloads');
|
||||||
|
|
||||||
if (downloads.length === 0) {
|
if (downloads.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<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 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 downloaded = progress.downloaded_mb ? `${progress.downloaded_mb.toFixed(1)} MB` : '0 MB';
|
||||||
const total = progress.total_mb ? `${progress.total_mb.toFixed(1)} MB` : 'Unknown';
|
const total = progress.total_mb ? `${progress.total_mb.toFixed(1)} MB` : 'Unknown';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card active">
|
<div class="download-card active">
|
||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
@ -238,7 +281,7 @@ class QueueManager {
|
|||||||
|
|
||||||
renderPendingQueue(queue) {
|
renderPendingQueue(queue) {
|
||||||
const container = document.getElementById('pending-queue');
|
const container = document.getElementById('pending-queue');
|
||||||
|
|
||||||
if (queue.length === 0) {
|
if (queue.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@ -255,7 +298,7 @@ class QueueManager {
|
|||||||
createPendingQueueCard(download, index) {
|
createPendingQueueCard(download, index) {
|
||||||
const addedAt = new Date(download.added_at).toLocaleString();
|
const addedAt = new Date(download.added_at).toLocaleString();
|
||||||
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
|
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card pending ${priorityClass}" data-id="${download.id}">
|
<div class="download-card pending ${priorityClass}" data-id="${download.id}">
|
||||||
<div class="queue-position">${index + 1}</div>
|
<div class="queue-position">${index + 1}</div>
|
||||||
@ -278,7 +321,7 @@ class QueueManager {
|
|||||||
|
|
||||||
renderCompletedDownloads(downloads) {
|
renderCompletedDownloads(downloads) {
|
||||||
const container = document.getElementById('completed-downloads');
|
const container = document.getElementById('completed-downloads');
|
||||||
|
|
||||||
if (downloads.length === 0) {
|
if (downloads.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@ -295,7 +338,7 @@ class QueueManager {
|
|||||||
createCompletedDownloadCard(download) {
|
createCompletedDownloadCard(download) {
|
||||||
const completedAt = new Date(download.completed_at).toLocaleString();
|
const completedAt = new Date(download.completed_at).toLocaleString();
|
||||||
const duration = this.calculateDuration(download.started_at, download.completed_at);
|
const duration = this.calculateDuration(download.started_at, download.completed_at);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card completed">
|
<div class="download-card completed">
|
||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
@ -314,7 +357,7 @@ class QueueManager {
|
|||||||
|
|
||||||
renderFailedDownloads(downloads) {
|
renderFailedDownloads(downloads) {
|
||||||
const container = document.getElementById('failed-downloads');
|
const container = document.getElementById('failed-downloads');
|
||||||
|
|
||||||
if (downloads.length === 0) {
|
if (downloads.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@ -331,7 +374,7 @@ class QueueManager {
|
|||||||
createFailedDownloadCard(download) {
|
createFailedDownloadCard(download) {
|
||||||
const failedAt = new Date(download.completed_at).toLocaleString();
|
const failedAt = new Date(download.completed_at).toLocaleString();
|
||||||
const retryCount = download.retry_count || 0;
|
const retryCount = download.retry_count || 0;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card failed">
|
<div class="download-card failed">
|
||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
@ -358,7 +401,20 @@ class QueueManager {
|
|||||||
const hasActive = (data.active_downloads || []).length > 0;
|
const hasActive = (data.active_downloads || []).length > 0;
|
||||||
const hasPending = (data.pending_queue || []).length > 0;
|
const hasPending = (data.pending_queue || []).length > 0;
|
||||||
const hasFailed = (data.failed_downloads || []).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('pause-all-btn').disabled = !hasActive;
|
||||||
document.getElementById('clear-queue-btn').disabled = !hasPending;
|
document.getElementById('clear-queue-btn').disabled = !hasPending;
|
||||||
document.getElementById('reorder-queue-btn').disabled = !hasPending || (data.pending_queue || []).length < 2;
|
document.getElementById('reorder-queue-btn').disabled = !hasPending || (data.pending_queue || []).length < 2;
|
||||||
@ -371,33 +427,33 @@ class QueueManager {
|
|||||||
completed: 'Clear Completed Downloads',
|
completed: 'Clear Completed Downloads',
|
||||||
failed: 'Clear Failed Downloads'
|
failed: 'Clear Failed Downloads'
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
pending: 'Are you sure you want to clear all pending downloads from the queue?',
|
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?',
|
completed: 'Are you sure you want to clear all completed downloads?',
|
||||||
failed: 'Are you sure you want to clear all failed downloads?'
|
failed: 'Are you sure you want to clear all failed downloads?'
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmed = await this.showConfirmModal(titles[type], messages[type]);
|
const confirmed = await this.showConfirmModal(titles[type], messages[type]);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.makeAuthenticatedRequest('/api/queue/clear', {
|
const response = await this.makeAuthenticatedRequest('/api/queue/clear', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ type })
|
body: JSON.stringify({ type })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
this.showToast(data.message, 'success');
|
this.showToast(data.message, 'success');
|
||||||
this.loadQueueData();
|
this.loadQueueData();
|
||||||
} else {
|
} else {
|
||||||
this.showToast(data.message, 'error');
|
this.showToast(data.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing queue:', error);
|
console.error('Error clearing queue:', error);
|
||||||
this.showToast('Failed to clear queue', 'error');
|
this.showToast('Failed to clear queue', 'error');
|
||||||
@ -411,17 +467,17 @@ class QueueManager {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ id: downloadId })
|
body: JSON.stringify({ id: downloadId })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
this.showToast('Download added back to queue', 'success');
|
this.showToast('Download added back to queue', 'success');
|
||||||
this.loadQueueData();
|
this.loadQueueData();
|
||||||
} else {
|
} else {
|
||||||
this.showToast(data.message, 'error');
|
this.showToast(data.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error retrying download:', error);
|
console.error('Error retrying download:', error);
|
||||||
this.showToast('Failed to retry download', 'error');
|
this.showToast('Failed to retry download', 'error');
|
||||||
@ -431,10 +487,10 @@ class QueueManager {
|
|||||||
async retryAllFailed() {
|
async retryAllFailed() {
|
||||||
const confirmed = await this.showConfirmModal('Retry All Failed Downloads', 'Are you sure you want to retry all failed downloads?');
|
const confirmed = await this.showConfirmModal('Retry All Failed Downloads', 'Are you sure you want to retry all failed downloads?');
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
// Get all failed downloads and retry them individually
|
// Get all failed downloads and retry them individually
|
||||||
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
||||||
|
|
||||||
for (const card of failedCards) {
|
for (const card of failedCards) {
|
||||||
const downloadId = card.dataset.id;
|
const downloadId = card.dataset.id;
|
||||||
if (downloadId) {
|
if (downloadId) {
|
||||||
@ -450,17 +506,17 @@ class QueueManager {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ id: downloadId })
|
body: JSON.stringify({ id: downloadId })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
this.showToast('Download removed from queue', 'success');
|
this.showToast('Download removed from queue', 'success');
|
||||||
this.loadQueueData();
|
this.loadQueueData();
|
||||||
} else {
|
} else {
|
||||||
this.showToast(data.message, 'error');
|
this.showToast(data.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error removing from queue:', error);
|
console.error('Error removing from queue:', error);
|
||||||
this.showToast('Failed to remove from queue', 'error');
|
this.showToast('Failed to remove from queue', 'error');
|
||||||
@ -471,21 +527,92 @@ class QueueManager {
|
|||||||
const start = new Date(startTime);
|
const start = new Date(startTime);
|
||||||
const end = new Date(endTime);
|
const end = new Date(endTime);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
|
|
||||||
const minutes = Math.floor(diffMs / (1000 * 60));
|
const minutes = Math.floor(diffMs / (1000 * 60));
|
||||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
return `${minutes}m ${seconds}s`;
|
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 = {}) {
|
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) {
|
if (response.status === 401) {
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,23 +621,23 @@ class QueueManager {
|
|||||||
document.getElementById('confirm-title').textContent = title;
|
document.getElementById('confirm-title').textContent = title;
|
||||||
document.getElementById('confirm-message').textContent = message;
|
document.getElementById('confirm-message').textContent = message;
|
||||||
document.getElementById('confirm-modal').classList.remove('hidden');
|
document.getElementById('confirm-modal').classList.remove('hidden');
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
resolve(false);
|
resolve(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
document.getElementById('confirm-ok').removeEventListener('click', handleConfirm);
|
document.getElementById('confirm-ok').removeEventListener('click', handleConfirm);
|
||||||
document.getElementById('confirm-cancel').removeEventListener('click', handleCancel);
|
document.getElementById('confirm-cancel').removeEventListener('click', handleCancel);
|
||||||
this.hideConfirmModal();
|
this.hideConfirmModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('confirm-ok').addEventListener('click', handleConfirm);
|
document.getElementById('confirm-ok').addEventListener('click', handleConfirm);
|
||||||
document.getElementById('confirm-cancel').addEventListener('click', handleCancel);
|
document.getElementById('confirm-cancel').addEventListener('click', handleCancel);
|
||||||
});
|
});
|
||||||
@ -523,7 +650,7 @@ class QueueManager {
|
|||||||
showToast(message, type = 'info') {
|
showToast(message, type = 'info') {
|
||||||
const container = document.getElementById('toast-container');
|
const container = document.getElementById('toast-container');
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
|
|
||||||
toast.className = `toast ${type}`;
|
toast.className = `toast ${type}`;
|
||||||
toast.innerHTML = `
|
toast.innerHTML = `
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
@ -533,9 +660,9 @@ class QueueManager {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(toast);
|
container.appendChild(toast);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (toast.parentElement) {
|
if (toast.parentElement) {
|
||||||
toast.remove();
|
toast.remove();
|
||||||
@ -553,7 +680,7 @@ class QueueManager {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
this.showToast('Logged out successfully', 'success');
|
this.showToast('Logged out successfully', 'success');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en" data-theme="light">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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 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">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@ -46,7 +48,7 @@
|
|||||||
<div class="stat-label">Total Items</div>
|
<div class="stat-label">Total Items</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-clock text-warning"></i>
|
<i class="fas fa-clock text-warning"></i>
|
||||||
@ -56,7 +58,7 @@
|
|||||||
<div class="stat-label">In Queue</div>
|
<div class="stat-label">In Queue</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-check-circle text-success"></i>
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
@ -66,7 +68,7 @@
|
|||||||
<div class="stat-label">Completed</div>
|
<div class="stat-label">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-exclamation-triangle text-error"></i>
|
<i class="fas fa-exclamation-triangle text-error"></i>
|
||||||
@ -77,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Speed and ETA -->
|
<!-- Speed and ETA -->
|
||||||
<div class="speed-eta-section">
|
<div class="speed-eta-section">
|
||||||
<div class="speed-info">
|
<div class="speed-info">
|
||||||
@ -115,7 +117,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="active-downloads-list" id="active-downloads">
|
<div class="active-downloads-list" id="active-downloads">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-pause-circle"></i>
|
<i class="fas fa-pause-circle"></i>
|
||||||
@ -132,6 +134,14 @@
|
|||||||
Download Queue
|
Download Queue
|
||||||
</h2>
|
</h2>
|
||||||
<div class="section-actions">
|
<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>
|
<button id="clear-queue-btn" class="btn btn-secondary" disabled>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
Clear Queue
|
Clear Queue
|
||||||
@ -142,7 +152,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pending-queue-list" id="pending-queue">
|
<div class="pending-queue-list" id="pending-queue">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
@ -165,7 +175,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="completed-downloads-list" id="completed-downloads">
|
<div class="completed-downloads-list" id="completed-downloads">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-check-circle"></i>
|
<i class="fas fa-check-circle"></i>
|
||||||
@ -192,7 +202,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="failed-downloads-list" id="failed-downloads">
|
<div class="failed-downloads-list" id="failed-downloads">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-check-circle text-success"></i>
|
<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="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>
|
<script src="{{ url_for('static', filename='js/queue.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
66
test_stop_functionality.py
Normal file
66
test_stop_functionality.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user