diff --git a/app/routers/router_downloads.py b/app/routers/router_downloads.py index daa830e..c432720 100644 --- a/app/routers/router_downloads.py +++ b/app/routers/router_downloads.py @@ -2,13 +2,15 @@ Download management routes for Ohm Stream Downloader API. """ +import json from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response +from pathlib import Path +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, Response from fastapi.templating import Jinja2Templates -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, FileResponse from app.download_manager import DownloadManager -from app.models import DownloadRequest +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 @@ -120,6 +122,73 @@ async def cancel_download( 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), diff --git a/static/css/style.css b/static/css/style.css index b222dc6..d982d7b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -646,3 +646,65 @@ h1 { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } + +/* ==================== Download Items ==================== */ +.download-item { + padding: 15px 18px; + background: var(--bg-card); + border-radius: var(--card-radius); + border-left: 4px solid var(--text-dim); + margin-bottom: 10px; + transition: var(--transition); +} + +.download-item:hover { + border-color: var(--primary); + transform: translateX(3px); +} + +.download-item.status-downloading { + border-left-color: var(--primary); + animation: pulse-border 2s ease-in-out infinite; +} + +.download-item.status-completed { + border-left-color: var(--accent); +} + +.download-item.status-failed, +.download-item.status-cancelled { + border-left-color: var(--danger); +} + +.download-item.status-paused { + border-left-color: #f0a500; +} + +.download-item.status-pending { + border-left-color: var(--text-dim); +} + +@keyframes pulse-border { + 0%, 100% { border-left-color: var(--primary); } + 50% { border-left-color: rgba(0, 217, 255, 0.3); } +} + +/* Progress bar shimmer */ +.download-item.status-downloading .progress-bar { + background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 50%, var(--primary) 100%); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* Download action buttons */ +.download-actions .btn-icon.warning { + color: #f0a500; +} +.download-actions .btn-icon.warning:hover { + background: rgba(240, 165, 0, 0.2); +} diff --git a/templates/components/downloads_list.html b/templates/components/downloads_list.html index f372d91..e49bfb9 100644 --- a/templates/components/downloads_list.html +++ b/templates/components/downloads_list.html @@ -1,7 +1,7 @@ {% if tasks %}
{% for task in tasks %} -
+
{{ task.filename }} {{ task.status | upper }} @@ -19,28 +19,38 @@
{% if task.status == 'downloading' or task.status == 'pending' %} - {% elif task.status == 'paused' %} - {% endif %} + {% if task.status == 'failed' or task.status == 'cancelled' %} + + {% endif %} + {% if task.status == 'completed' %} - - + + - + {% endif %} @@ -51,6 +61,6 @@ {% else %}
-

Aucun téléchargement en cours

+

Aucun telechargement en cours

{% endif %} diff --git a/templates/components/downloads_section.html b/templates/components/downloads_section.html index c241076..2069927 100644 --- a/templates/components/downloads_section.html +++ b/templates/components/downloads_section.html @@ -1,12 +1,20 @@
-

📥 Téléchargements

+

Telechargements

+
@@ -17,12 +25,20 @@ hx-trigger="load, refresh, every 3s" hx-swap="innerHTML">
-
Chargement des téléchargements... +
Chargement des telechargements...