Files
Roman 9500a84a5e Téléchargements : suppression par épisode et regroupement par animé
- 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
2026-09-22 12:17:27 +00:00

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