- DELETE /api/downloads/{id} (?delete_file=true pour effacer aussi le fichier)
- Événement SSE « removed » → toutes les pages ouvertes se synchronisent
- Page Téléchargements groupée par animé : groupes repliables (dépliés si
actifs), compteurs par statut, taille totale, tri par numéro d'épisode
- Actions par groupe : 🧹 retirer les terminés, ✖ annuler les actifs
- Limite de liste 200 → 2000 tâches
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
"""Gestion de la file de téléchargements + progression temps réel (SSE)."""
|
|
|
|
import json
|
|
import logging
|
|
from collections.abc import AsyncIterator
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sse_starlette.sse import EventSourceResponse
|
|
|
|
from app.routers.auth import current_user
|
|
from app.scrapers.base import decode_internal_url
|
|
from app.services.downloads import download_manager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/api/downloads", tags=["downloads"], dependencies=[Depends(current_user)]
|
|
)
|
|
|
|
|
|
class EnqueueRequest(BaseModel):
|
|
internal_url: str # format `video_url|page_url|titre`
|
|
|
|
|
|
@router.get("")
|
|
async def list_downloads() -> list[dict]:
|
|
return await download_manager.list_all()
|
|
|
|
|
|
@router.post("", status_code=201)
|
|
async def enqueue(payload: EnqueueRequest) -> dict:
|
|
try:
|
|
video_url, page_url, title = decode_internal_url(payload.internal_url)
|
|
except ValueError as exc:
|
|
raise HTTPException(422, detail=str(exc)) from exc
|
|
return await download_manager.enqueue(video_url, page_url, title)
|
|
|
|
|
|
@router.post("/{download_id}/pause")
|
|
async def pause(download_id: int) -> dict:
|
|
await download_manager.pause(download_id)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{download_id}/resume")
|
|
async def resume(download_id: int) -> dict:
|
|
if not await download_manager.resume(download_id):
|
|
raise HTTPException(409, detail="Ce téléchargement n'est pas en pause")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{download_id}/retry")
|
|
async def retry(download_id: int) -> dict:
|
|
if not await download_manager.retry(download_id):
|
|
raise HTTPException(
|
|
409, detail="Seules les tâches en échec/annulées peuvent être relancées"
|
|
)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/{download_id}/cancel")
|
|
async def cancel(download_id: int) -> dict:
|
|
await download_manager.cancel(download_id)
|
|
return {"ok": True}
|
|
|
|
@router.delete("/{download_id}")
|
|
async def delete_download(download_id: int, delete_file: bool = False) -> dict:
|
|
"""Retire une tâche de la file ; delete_file=true efface aussi le fichier."""
|
|
if not await download_manager.delete(download_id, delete_file=delete_file):
|
|
raise HTTPException(404, detail="Téléchargement introuvable")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/cancel-all")
|
|
async def cancel_all() -> dict:
|
|
return {"cancelled": await download_manager.cancel_all()}
|
|
|
|
|
|
@router.post("/clear-finished")
|
|
async def clear_finished() -> dict:
|
|
return {"removed": await download_manager.clear_finished()}
|
|
|
|
|
|
@router.get("/events")
|
|
async def events() -> EventSourceResponse:
|
|
"""Flux SSE : progression de tous les téléchargements en temps réel."""
|
|
|
|
async def stream() -> AsyncIterator[dict]:
|
|
yield {"data": json.dumps({"type": "snapshot", "items": await download_manager.list_all()})}
|
|
async for message in download_manager.subscribe():
|
|
yield {"data": json.dumps(message, default=str)}
|
|
|
|
return EventSourceResponse(stream())
|