v0.1.0 — Réécriture complète de OhmStreaming

Nouvelle version réécrite de zéro : recherche multi-sources (Vostfree,
French-Manga), extraction 2 niveaux, proxy vidéo intégré, streaming/téléchargement
HLS, métadonnées Kitsu, bibliothèque locale, comptes JWT + administration,
découverte fusionnée, indexeur Torznab (Sonarr/Prowlarr).
This commit is contained in:
Roman
2026-09-22 10:05:47 +00:00
commit 41566ab5fb
66 changed files with 8895 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
"""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.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 update in download_manager.subscribe():
yield {"data": json.dumps({"type": "update", "item": update}, default=str)}
return EventSourceResponse(stream())