feat: frontend modernization with HTMX, Alpine.js and Plyr (Phase 3)
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

- Integrated HTMX for server-driven UI updates and fragments
- Adopted Alpine.js for global reactive state and tab management
- Replaced legacy player with Plyr.io for premium streaming experience
- Implemented real-time download polling via HTMX
- Added server-sent Toast notification system
- Fixed navigation and authentication scoping issues
This commit is contained in:
root
2026-03-24 11:10:22 +00:00
parent 2b4cc617cb
commit 5c7116557d
17 changed files with 584 additions and 690 deletions
+50 -97
View File
@@ -2,56 +2,48 @@
Download management routes for Ohm Stream Downloader API.
"""
import os
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from fastapi.responses import FileResponse
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.templating import Jinja2Templates
from app.download_manager import DownloadManager
from app.models import DownloadRequest, DownloadStatus
from app.utils import is_safe_filename, sanitize_filename
from app.models import DownloadRequest
from app.routers.router_auth import get_current_user_from_token
router = APIRouter(prefix="/api/download", tags=["downloads"])
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),
):
"""Get list of all download tasks"""
tasks = download_manager.get_all_tasks()
if html or request.headers.get("HX-Request"):
return templates.TemplateResponse(
"components/downloads_list.html",
{"request": request, "tasks": tasks}
)
return tasks
@router.post("")
async def create_download(
request: DownloadRequest,
background_tasks: BackgroundTasks,
download_request: DownloadRequest,
download_manager: DownloadManager = Depends(get_download_manager),
current_user=Depends(get_current_user_from_token),
):
"""Create a new download task"""
if request.filename:
request.filename = sanitize_filename(request.filename)
if not is_safe_filename(request.filename):
raise HTTPException(
status_code=400,
detail="Invalid filename. Path traversal attempts are not allowed.",
)
task = download_manager.create_task(request)
background_tasks.add_task(download_manager.start_download, task.id)
return {"task_id": task.id, "task": task}
@router.get("/direct")
async def direct_download(
url: str,
filename: str,
background_tasks: BackgroundTasks,
download_manager: DownloadManager = Depends(get_download_manager),
):
"""Download directly from a video URL with custom filename"""
request = DownloadRequest(url=url, filename=filename)
task = download_manager.create_task(request)
background_tasks.add_task(download_manager.start_download, task.id)
return {"task_id": task.id, "task": task}
return download_manager.create_task(download_request)
@router.get("/{task_id}")
@@ -59,7 +51,7 @@ async def get_download_status(
task_id: str,
download_manager: DownloadManager = Depends(get_download_manager),
):
"""Get status of a specific download"""
"""Get status of a specific download task"""
task = download_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
@@ -70,82 +62,43 @@ async def get_download_status(
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 = download_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
await download_manager.pause_download(task_id)
return {"status": "paused"}
"""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,
background_tasks: BackgroundTasks,
download_manager: DownloadManager = Depends(get_download_manager),
current_user=Depends(get_current_user_from_token),
):
"""Resume a paused download"""
task = download_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
if task.status == DownloadStatus.PAUSED:
background_tasks.add_task(download_manager.start_download, task_id)
return {"status": "resumed"}
return {"status": "already running or completed"}
"""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 delete_download(
async def cancel_download(
task_id: str,
download_manager: DownloadManager = Depends(get_download_manager),
current_user=Depends(get_current_user_from_token),
):
"""Delete/cancel a download"""
task = download_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
await download_manager.delete_task(task_id)
return {"status": "deleted"}
"""Cancel and delete a download task"""
if download_manager.cancel_download(task_id):
return {"status": "success", "message": "Download cancelled"}
raise HTTPException(status_code=400, detail="Failed to cancel download")
@router.get("/{task_id}/file")
async def download_file(
task_id: str,
@router.post("/cleanup")
async def cleanup_completed(
download_manager: DownloadManager = Depends(get_download_manager),
current_user=Depends(get_current_user_from_token),
):
"""Download the completed file"""
task = download_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
if task.status != DownloadStatus.COMPLETED:
raise HTTPException(status_code=400, detail="Download not completed")
if not task.file_path or not os.path.exists(task.file_path):
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(
task.file_path, filename=task.filename, media_type="application/octet-stream"
)
@router.get("/")
async def list_downloads(
download_manager: DownloadManager = Depends(get_download_manager),
):
"""List all download tasks"""
return {"downloads": download_manager.get_all_tasks()}
# Legacy endpoint for /api/downloads
legacy_router = APIRouter(prefix="/api", tags=["downloads-legacy"])
@legacy_router.get("/downloads")
async def list_all_downloads(
download_manager: DownloadManager = Depends(get_download_manager),
):
"""List all download tasks (legacy endpoint at /api/downloads)"""
return {"downloads": download_manager.get_all_tasks()}
"""Remove all completed tasks from the list"""
count = download_manager.cleanup_tasks()
return {"status": "success", "message": f"Cleaned up {count} tasks"}