9f9df600c1
- Route GET /api/downloads/video/{task_id} pour streamer les videos
- Route POST /api/downloads/{task_id}/retry pour relancer les failed
- Route POST /api/downloads/cancel-all pour annuler tous les actifs
- Barre de progression animee (shimmer + pulse)
- Indicateurs visuels par status (bordures colorees)
- Bouton Retry pour telechargements echoues/annules
- Actions groupees (Nettoyer termines, Tout annuler)
- Compteur de telechargements actifs
- hx-on::after-request pour refresh auto
Closes #17, Closes #8
206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
"""
|
|
Download management routes for Ohm Stream Downloader API.
|
|
"""
|
|
|
|
import json
|
|
from typing import Optional
|
|
from pathlib import Path
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, Response
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.responses import HTMLResponse, FileResponse
|
|
|
|
from app.download_manager import DownloadManager
|
|
from app.models import DownloadRequest, DownloadStatus
|
|
from app.models.auth import User
|
|
from app.routers.router_auth import get_current_user_from_token, get_optional_user
|
|
|
|
router = APIRouter(prefix="/api/downloads", tags=["downloads"])
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
def get_download_manager() -> DownloadManager:
|
|
from main import download_manager
|
|
return download_manager
|
|
|
|
|
|
@router.get("")
|
|
async def get_downloads(
|
|
request: Request,
|
|
html: bool = Query(False),
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user: Optional[User] = Depends(get_optional_user),
|
|
):
|
|
"""Get list of all download tasks. Returns HTML for HTMX requests."""
|
|
is_htmx = request.headers.get("HX-Request") == "true" or request.headers.get("HX-Request")
|
|
|
|
if current_user is None and (html or is_htmx):
|
|
return templates.TemplateResponse(
|
|
"components/login_prompt.html", {"request": request}
|
|
)
|
|
|
|
if current_user is None:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
tasks = download_manager.get_all_tasks()
|
|
|
|
if html or is_htmx:
|
|
print(f"[DOWNLOADS] HTML Request. Found {len(tasks)} tasks.")
|
|
return templates.TemplateResponse(
|
|
"components/downloads_list.html",
|
|
{"request": request, "tasks": tasks}
|
|
)
|
|
|
|
print(f"[DOWNLOADS] API Request. Returning JSON.")
|
|
return {"downloads": tasks}
|
|
|
|
|
|
@router.post("")
|
|
async def create_download(
|
|
download_request: DownloadRequest,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user=Depends(get_current_user_from_token),
|
|
):
|
|
"""Create a new download task"""
|
|
return download_manager.create_task(download_request)
|
|
|
|
|
|
@router.get("/{task_id}")
|
|
async def get_download_status(
|
|
task_id: str,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user: Optional[User] = Depends(get_optional_user),
|
|
):
|
|
"""Get status of a specific download task"""
|
|
if current_user is None:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
task = download_manager.get_task(task_id)
|
|
if not task:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
return task
|
|
|
|
|
|
@router.post("/{task_id}/pause")
|
|
async def pause_download(
|
|
task_id: str,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user=Depends(get_current_user_from_token),
|
|
):
|
|
"""Pause a download task"""
|
|
if download_manager.pause_download(task_id):
|
|
return {"status": "success", "message": "Download paused"}
|
|
raise HTTPException(status_code=400, detail="Failed to pause download")
|
|
|
|
|
|
@router.post("/{task_id}/resume")
|
|
async def resume_download(
|
|
task_id: str,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user=Depends(get_current_user_from_token),
|
|
):
|
|
"""Resume a paused download task"""
|
|
if download_manager.resume_download(task_id):
|
|
return {"status": "success", "message": "Download resumed"}
|
|
raise HTTPException(status_code=400, detail="Failed to resume download")
|
|
|
|
|
|
@router.delete("/{task_id}")
|
|
async def cancel_download(
|
|
task_id: str,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user=Depends(get_current_user_from_token),
|
|
):
|
|
"""Cancel and delete a download task"""
|
|
if hasattr(download_manager, "cancel_download"):
|
|
if download_manager.cancel_download(task_id):
|
|
return {"status": "success", "message": "Download cancelled"}
|
|
|
|
if task_id in download_manager.tasks:
|
|
del download_manager.tasks[task_id]
|
|
return {"status": "success", "message": "Download removed"}
|
|
|
|
raise HTTPException(status_code=400, detail="Failed to cancel download")
|
|
|
|
|
|
@router.get("/video/{task_id}")
|
|
async def stream_video(
|
|
task_id: str,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user: Optional[User] = Depends(get_optional_user),
|
|
):
|
|
"""Stream a completed download as video"""
|
|
if current_user is None:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
task = download_manager.get_task(task_id)
|
|
if not task:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
if task.status != DownloadStatus.COMPLETED or not task.file_path:
|
|
raise HTTPException(status_code=400, detail="Download not completed")
|
|
file_path = Path(task.file_path)
|
|
if not file_path.exists():
|
|
raise HTTPException(status_code=404, detail="File not found on disk")
|
|
media_types = {
|
|
".mp4": "video/mp4", ".mkv": "video/x-matroska", ".avi": "video/x-msvideo",
|
|
".webm": "video/webm", ".flv": "video/x-flv",
|
|
}
|
|
media_type = media_types.get(file_path.suffix.lower(), "video/mp4")
|
|
return FileResponse(str(file_path), media_type=media_type)
|
|
|
|
|
|
@router.post("/{task_id}/retry")
|
|
async def retry_download(
|
|
task_id: str,
|
|
background_tasks: BackgroundTasks,
|
|
response: Response,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""Retry a failed or cancelled download"""
|
|
task = download_manager.get_task(task_id)
|
|
if not task:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
if task.status not in ("failed", "cancelled"):
|
|
raise HTTPException(status_code=400, detail="Can only retry failed or cancelled downloads")
|
|
task.status = DownloadStatus.PENDING
|
|
task.progress = 0.0
|
|
if hasattr(download_manager, "_process_download"):
|
|
background_tasks.add_task(download_manager._process_download, task_id)
|
|
response.headers["HX-Trigger"] = json.dumps(
|
|
{"show-toast": {"message": "Telechargement relance", "type": "success"}}
|
|
)
|
|
return {"status": "retrying"}
|
|
|
|
|
|
@router.post("/cancel-all")
|
|
async def cancel_all_downloads(
|
|
response: Response,
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""Cancel all active downloads"""
|
|
count = 0
|
|
for tid, task in list(download_manager.tasks.items()):
|
|
if task.status in ("downloading", "pending"):
|
|
download_manager.cancel_download(tid) if hasattr(download_manager, "cancel_download") else download_manager.tasks.pop(tid, None)
|
|
count += 1
|
|
response.headers["HX-Trigger"] = json.dumps(
|
|
{"show-toast": {"message": f"{count} telechargement(s) annule(s)", "type": "info"}}
|
|
)
|
|
return {"status": "cancelled", "count": count}
|
|
|
|
|
|
@router.post("/cleanup")
|
|
async def cleanup_completed(
|
|
download_manager: DownloadManager = Depends(get_download_manager),
|
|
current_user=Depends(get_current_user_from_token),
|
|
):
|
|
"""Remove all completed tasks from the list"""
|
|
if hasattr(download_manager, "cleanup_tasks"):
|
|
count = download_manager.cleanup_tasks()
|
|
return {"status": "success", "message": f"Cleaned up {count} tasks"}
|
|
|
|
to_delete = [tid for tid, t in download_manager.tasks.items() if t.status == "completed"]
|
|
for tid in to_delete:
|
|
del download_manager.tasks[tid]
|
|
return {"status": "success", "message": f"Cleaned up {len(to_delete)} tasks"}
|