"""Download queue API endpoints for Aniworld web application. This module provides REST API endpoints for managing the anime download queue, including adding episodes, removing items, controlling queue processing, and retrieving queue status and statistics. """ from fastapi import APIRouter, Depends, HTTPException, Path, status from fastapi.responses import JSONResponse from src.server.models.download import ( DownloadRequest, QueueOperationRequest, QueueStatusResponse, ) from src.server.services.download_service import DownloadService, DownloadServiceError from src.server.utils.dependencies import get_download_service, require_auth router = APIRouter(prefix="/api/queue", tags=["download"]) @router.get("/status", response_model=QueueStatusResponse) async def get_queue_status( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Get current download queue status and statistics. Returns comprehensive information about all queue items including: - Active downloads with progress - Pending items waiting to be processed - Recently completed downloads - Failed downloads Requires authentication. Returns: QueueStatusResponse: Complete queue status and statistics Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: queue_status = await download_service.get_queue_status() queue_stats = await download_service.get_queue_stats() # Build response matching QueueStatusResponse model response = QueueStatusResponse( status=queue_status, statistics=queue_stats, ) return response except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve queue status: {str(e)}", ) @router.post("/add", status_code=status.HTTP_201_CREATED) async def add_to_queue( request: DownloadRequest, _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Add episodes to the download queue. Adds one or more episodes to the download queue with specified priority. Episodes are validated and queued for processing based on priority level: - HIGH priority items are processed first - NORMAL and LOW priority items follow FIFO order Requires authentication. Args: request: Download request with serie info, episodes, and priority Returns: DownloadResponse: Status and list of created download item IDs Raises: HTTPException: 401 if not authenticated, 400 for invalid request, 500 on service error """ try: # Validate request if not request.episodes: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="At least one episode must be specified", ) # Add to queue added_ids = await download_service.add_to_queue( serie_id=request.serie_id, serie_folder=request.serie_folder, serie_name=request.serie_name, episodes=request.episodes, priority=request.priority, ) # Keep a backwards-compatible response shape and return it as a # raw JSONResponse so FastAPI won't coerce it based on any # response_model defined elsewhere. payload = { "status": "success", "message": f"Added {len(added_ids)} episode(s) to download queue", "added_items": added_ids, "item_ids": added_ids, "failed_items": [], } return JSONResponse( content=payload, status_code=status.HTTP_201_CREATED, ) except DownloadServiceError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to add episodes to queue: {str(e)}", ) @router.delete("/completed", status_code=status.HTTP_200_OK) async def clear_completed( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Clear completed downloads from history. Removes all completed download items from the queue history. This helps keep the queue display clean and manageable. Requires authentication. Returns: dict: Status message with count of cleared items Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: cleared_count = await download_service.clear_completed() return { "status": "success", "message": f"Cleared {cleared_count} completed item(s)", "count": cleared_count, } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to clear completed items: {str(e)}", ) @router.delete("/failed", status_code=status.HTTP_200_OK) async def clear_failed( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Clear failed downloads from history. Removes all failed download items from the queue history. This helps keep the queue display clean and manageable. Requires authentication. Returns: dict: Status message with count of cleared items Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: cleared_count = await download_service.clear_failed() return { "status": "success", "message": f"Cleared {cleared_count} failed item(s)", "count": cleared_count, } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to clear failed items: {str(e)}", ) @router.delete("/pending", status_code=status.HTTP_200_OK) async def clear_pending( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Clear all pending downloads from the queue. Removes all pending download items from the queue. This is useful for clearing the entire queue at once instead of removing items one by one. Requires authentication. Returns: dict: Status message with count of cleared items Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: cleared_count = await download_service.clear_pending() return { "status": "success", "message": f"Removed {cleared_count} pending item(s)", "count": cleared_count, } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to clear pending items: {str(e)}", ) @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_from_queue( item_id: str = Path(..., description="Download item ID to remove"), _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Remove a specific item from the download queue. Removes a download item from the queue. If the item is currently downloading, it will be cancelled and marked as cancelled. If it's pending, it will simply be removed from the queue. Requires authentication. Args: item_id: Unique identifier of the download item to remove Raises: HTTPException: 401 if not authenticated, 404 if item not found, 500 on service error """ try: removed_ids = await download_service.remove_from_queue([item_id]) if not removed_ids: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Download item {item_id} not found in queue", ) except DownloadServiceError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to remove item from queue: {str(e)}", ) @router.delete("/", status_code=status.HTTP_204_NO_CONTENT) async def remove_multiple_from_queue( request: QueueOperationRequest, _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Remove multiple items from the download queue. Removes multiple download items from the queue based on provided IDs. Items that are currently downloading will be cancelled. Requires authentication. Args: request: Request containing list of item IDs to remove Raises: HTTPException: 401 if not authenticated, 404 if no items found, 500 on service error """ try: removed_ids = await download_service.remove_from_queue( request.item_ids ) if not removed_ids: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No matching items found in queue", ) except DownloadServiceError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to remove items from queue: {str(e)}", ) @router.post("/start", status_code=status.HTTP_200_OK) async def start_queue( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Start automatic queue processing. Starts processing all pending downloads sequentially, one at a time. The queue will continue processing until all items are complete or the queue is manually stopped. Processing continues even if the browser is closed. Only one download can be active at a time. If a download is already active or queue processing is running, an error is returned. Requires authentication. Returns: dict: Status message confirming queue processing started Raises: HTTPException: 401 if not authenticated, 400 if queue is empty or processing already active, 500 on service error """ try: result = await download_service.start_queue_processing() if result is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No pending downloads in queue", ) return { "status": "success", "message": "Queue processing started", } except DownloadServiceError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to start queue processing: {str(e)}", ) @router.post("/stop", status_code=status.HTTP_200_OK) async def stop_queue( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Stop processing new downloads from queue. Prevents new downloads from starting. The current active download will continue to completion, but no new downloads will be started from the pending queue. Requires authentication. Returns: dict: Status message indicating queue processing has been stopped Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: await download_service.stop_downloads() return { "status": "success", "message": ( "Queue processing stopped (current download will continue)" ), } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to stop queue processing: {str(e)}", ) @router.post("/pause", status_code=status.HTTP_200_OK) async def pause_queue( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Pause queue processing (alias for stop). Prevents new downloads from starting. The current active download will continue to completion, but no new downloads will be started from the pending queue. Requires authentication. Returns: dict: Status message indicating queue processing has been paused Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: await download_service.stop_downloads() return { "status": "success", "message": "Queue processing paused", } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to pause queue processing: {str(e)}", ) @router.post("/reorder", status_code=status.HTTP_200_OK) async def reorder_queue( request: QueueOperationRequest, _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Reorder items in the pending queue. Reorders the pending queue based on the provided list of item IDs. Items will be placed in the order specified by the item_ids list. Items not included in the list will remain at the end of the queue. Requires authentication. Args: request: List of download item IDs in desired order Returns: dict: Status message Raises: HTTPException: 401 if not authenticated, 404 if no items match, 500 on service error """ try: await download_service.reorder_queue(request.item_ids) return { "status": "success", "message": f"Queue reordered with {len(request.item_ids)} items", } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to reorder queue: {str(e)}", ) @router.post("/retry", status_code=status.HTTP_200_OK) async def retry_failed( request: QueueOperationRequest, _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): """Retry failed downloads. Moves failed download items back to the pending queue for retry. Only items that haven't exceeded the maximum retry count will be retried. Requires authentication. Args: request: List of download item IDs to retry (empty list retries all) Returns: dict: Status message with count of retried items Raises: HTTPException: 401 if not authenticated, 500 on service error """ try: # If no specific IDs provided, retry all failed items item_ids = request.item_ids if request.item_ids else None retried_ids = await download_service.retry_failed(item_ids) return { "status": "success", "message": f"Retrying {len(retried_ids)} failed item(s)", "retried_count": len(retried_ids), "retried_ids": retried_ids, } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retry downloads: {str(e)}", )