303 lines
11 KiB
Python
303 lines
11 KiB
Python
from flask import Blueprint, render_template, request, jsonify
|
|
from web.controllers.auth_controller import optional_auth
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
|
|
# Create blueprint for download queue management
|
|
download_queue_bp = Blueprint('download_queue', __name__)
|
|
|
|
# Global download queue state
|
|
download_queue_state = {
|
|
'active_downloads': [],
|
|
'pending_queue': [],
|
|
'completed_downloads': [],
|
|
'failed_downloads': [],
|
|
'queue_lock': threading.Lock(),
|
|
'statistics': {
|
|
'total_items': 0,
|
|
'completed_items': 0,
|
|
'failed_items': 0,
|
|
'estimated_time_remaining': None,
|
|
'current_speed': '0 MB/s',
|
|
'average_speed': '0 MB/s'
|
|
}
|
|
}
|
|
|
|
@download_queue_bp.route('/queue')
|
|
@optional_auth
|
|
def queue_page():
|
|
"""Download queue management page."""
|
|
return render_template('queue.html')
|
|
|
|
@download_queue_bp.route('/api/queue/status')
|
|
@optional_auth
|
|
def get_queue_status():
|
|
"""Get detailed download queue status."""
|
|
with download_queue_state['queue_lock']:
|
|
# Calculate ETA
|
|
eta = None
|
|
if download_queue_state['active_downloads']:
|
|
active_download = download_queue_state['active_downloads'][0]
|
|
if 'progress' in active_download and active_download['progress'].get('speed_mbps', 0) > 0:
|
|
remaining_items = len(download_queue_state['pending_queue'])
|
|
avg_speed = active_download['progress']['speed_mbps']
|
|
# Rough estimation: assume 500MB per episode
|
|
estimated_mb_remaining = remaining_items * 500
|
|
eta_seconds = estimated_mb_remaining / avg_speed if avg_speed > 0 else None
|
|
if eta_seconds:
|
|
eta = datetime.now() + timedelta(seconds=eta_seconds)
|
|
|
|
return jsonify({
|
|
'active_downloads': download_queue_state['active_downloads'],
|
|
'pending_queue': download_queue_state['pending_queue'],
|
|
'completed_downloads': download_queue_state['completed_downloads'][-10:], # Last 10
|
|
'failed_downloads': download_queue_state['failed_downloads'][-10:], # Last 10
|
|
'statistics': {
|
|
**download_queue_state['statistics'],
|
|
'eta': eta.isoformat() if eta else None
|
|
}
|
|
})
|
|
|
|
@download_queue_bp.route('/api/queue/clear', methods=['POST'])
|
|
@optional_auth
|
|
def clear_queue():
|
|
"""Clear completed and failed downloads from queue."""
|
|
try:
|
|
data = request.get_json() or {}
|
|
queue_type = data.get('type', 'completed') # 'completed', 'failed', or 'all'
|
|
|
|
with download_queue_state['queue_lock']:
|
|
if queue_type == 'completed' or queue_type == 'all':
|
|
download_queue_state['completed_downloads'].clear()
|
|
|
|
if queue_type == 'failed' or queue_type == 'all':
|
|
download_queue_state['failed_downloads'].clear()
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': f'Cleared {queue_type} downloads'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@download_queue_bp.route('/api/queue/retry', methods=['POST'])
|
|
@optional_auth
|
|
def retry_failed_download():
|
|
"""Retry a failed download."""
|
|
try:
|
|
data = request.get_json()
|
|
download_id = data.get('id')
|
|
|
|
if not download_id:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Download ID is required'
|
|
}), 400
|
|
|
|
with download_queue_state['queue_lock']:
|
|
# Find failed download
|
|
failed_download = None
|
|
for i, download in enumerate(download_queue_state['failed_downloads']):
|
|
if download['id'] == download_id:
|
|
failed_download = download_queue_state['failed_downloads'].pop(i)
|
|
break
|
|
|
|
if not failed_download:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Failed download not found'
|
|
}), 404
|
|
|
|
# Reset download status and add back to queue
|
|
failed_download['status'] = 'queued'
|
|
failed_download['error'] = None
|
|
failed_download['retry_count'] = failed_download.get('retry_count', 0) + 1
|
|
download_queue_state['pending_queue'].append(failed_download)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Download added back to queue'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@download_queue_bp.route('/api/queue/remove', methods=['POST'])
|
|
@optional_auth
|
|
def remove_from_queue():
|
|
"""Remove an item from the pending queue."""
|
|
try:
|
|
data = request.get_json()
|
|
download_id = data.get('id')
|
|
|
|
if not download_id:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Download ID is required'
|
|
}), 400
|
|
|
|
with download_queue_state['queue_lock']:
|
|
# Find and remove from pending queue
|
|
removed = False
|
|
for i, download in enumerate(download_queue_state['pending_queue']):
|
|
if download['id'] == download_id:
|
|
download_queue_state['pending_queue'].pop(i)
|
|
removed = True
|
|
break
|
|
|
|
if not removed:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Download not found in queue'
|
|
}), 404
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Download removed from queue'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@download_queue_bp.route('/api/queue/reorder', methods=['POST'])
|
|
@optional_auth
|
|
def reorder_queue():
|
|
"""Reorder items in the pending queue."""
|
|
try:
|
|
data = request.get_json()
|
|
new_order = data.get('order') # Array of download IDs in new order
|
|
|
|
if not new_order or not isinstance(new_order, list):
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Valid order array is required'
|
|
}), 400
|
|
|
|
with download_queue_state['queue_lock']:
|
|
# Create new queue based on the provided order
|
|
old_queue = download_queue_state['pending_queue'].copy()
|
|
new_queue = []
|
|
|
|
# Add items in the specified order
|
|
for download_id in new_order:
|
|
for download in old_queue:
|
|
if download['id'] == download_id:
|
|
new_queue.append(download)
|
|
break
|
|
|
|
# Add any remaining items that weren't in the new order
|
|
for download in old_queue:
|
|
if download not in new_queue:
|
|
new_queue.append(download)
|
|
|
|
download_queue_state['pending_queue'] = new_queue
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Queue reordered successfully'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
# Helper functions for queue management
|
|
def add_to_download_queue(serie_name, episode_info, priority='normal'):
|
|
"""Add a download to the queue."""
|
|
import uuid
|
|
|
|
download_item = {
|
|
'id': str(uuid.uuid4()),
|
|
'serie_name': serie_name,
|
|
'episode': episode_info,
|
|
'status': 'queued',
|
|
'priority': priority,
|
|
'added_at': datetime.now().isoformat(),
|
|
'started_at': None,
|
|
'completed_at': None,
|
|
'error': None,
|
|
'retry_count': 0,
|
|
'progress': {
|
|
'percent': 0,
|
|
'downloaded_mb': 0,
|
|
'total_mb': 0,
|
|
'speed_mbps': 0,
|
|
'eta_seconds': None
|
|
}
|
|
}
|
|
|
|
with download_queue_state['queue_lock']:
|
|
# Insert based on priority
|
|
if priority == 'high':
|
|
download_queue_state['pending_queue'].insert(0, download_item)
|
|
else:
|
|
download_queue_state['pending_queue'].append(download_item)
|
|
|
|
download_queue_state['statistics']['total_items'] += 1
|
|
|
|
return download_item['id']
|
|
|
|
def update_download_progress(download_id, progress_data):
|
|
"""Update progress for an active download."""
|
|
with download_queue_state['queue_lock']:
|
|
for download in download_queue_state['active_downloads']:
|
|
if download['id'] == download_id:
|
|
download['progress'].update(progress_data)
|
|
|
|
# Update global statistics
|
|
if 'speed_mbps' in progress_data:
|
|
download_queue_state['statistics']['current_speed'] = f"{progress_data['speed_mbps']:.1f} MB/s"
|
|
|
|
break
|
|
|
|
def move_download_to_completed(download_id, success=True, error=None):
|
|
"""Move download from active to completed/failed."""
|
|
with download_queue_state['queue_lock']:
|
|
download = None
|
|
for i, item in enumerate(download_queue_state['active_downloads']):
|
|
if item['id'] == download_id:
|
|
download = download_queue_state['active_downloads'].pop(i)
|
|
break
|
|
|
|
if download:
|
|
download['completed_at'] = datetime.now().isoformat()
|
|
|
|
if success:
|
|
download['status'] = 'completed'
|
|
download['progress']['percent'] = 100
|
|
download_queue_state['completed_downloads'].append(download)
|
|
download_queue_state['statistics']['completed_items'] += 1
|
|
else:
|
|
download['status'] = 'failed'
|
|
download['error'] = error
|
|
download_queue_state['failed_downloads'].append(download)
|
|
download_queue_state['statistics']['failed_items'] += 1
|
|
|
|
def start_next_download():
|
|
"""Move next queued download to active state."""
|
|
with download_queue_state['queue_lock']:
|
|
if download_queue_state['pending_queue'] and len(download_queue_state['active_downloads']) < 3: # Max 3 concurrent
|
|
download = download_queue_state['pending_queue'].pop(0)
|
|
download['status'] = 'downloading'
|
|
download['started_at'] = datetime.now().isoformat()
|
|
download_queue_state['active_downloads'].append(download)
|
|
return download
|
|
return None
|
|
|
|
def get_queue_statistics():
|
|
"""Get current queue statistics."""
|
|
with download_queue_state['queue_lock']:
|
|
return download_queue_state['statistics'].copy() |