524 lines
16 KiB
Python
524 lines
16 KiB
Python
"""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)}",
|
|
)
|