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