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