From 05fb0081c6a59fd124f774457ee0d8371d64f0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastjan=20Arti=C4=8D?= Date: Wed, 29 Apr 2026 10:24:23 +0000 Subject: [PATCH] Fix preview cutoff + sticky left panel 1. Preview endpoint now supports HTTP Range requests (HTTP 206 Partial) - HTML5 video player needs Range support to seek/buffer properly - Without it, video would cut off after a few seconds - Returns chunks of 64KB on demand 2. Left panel (upload form) is now sticky (position: sticky) - Stays in view while right panel (jobs list) scrolls - On mobile (<800px) reverts to normal flow --- app/main.py | 51 +++++++++++++++++++++++++++++++++++++++++--- templates/index.html | 11 ++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 63cea69..30a1300 100644 --- a/app/main.py +++ b/app/main.py @@ -29,7 +29,7 @@ from fastapi import ( BackgroundTasks, Request, status ) from fastapi.responses import ( - FileResponse, HTMLResponse, StreamingResponse, JSONResponse + FileResponse, HTMLResponse, StreamingResponse, JSONResponse, Response ) from fastapi.staticfiles import StaticFiles from fastapi.security import HTTPBasic, HTTPBasicCredentials @@ -667,14 +667,59 @@ async def download(job_id: str, user: str = Depends(check_auth)): @app.get("/api/preview/{job_id}") -async def preview(job_id: str, user: str = Depends(check_auth)): +async def preview(job_id: str, request: Request, user: str = Depends(check_auth)): + """Video preview z Range request podporo (potrebno za HTML5 video player).""" job = load_job(job_id) if not job or job.get("status") != "done": raise HTTPException(404, "Ne pripravljen") out = Path(job["output_path"]) if not out.exists(): raise HTTPException(404, "Output ne obstaja") - return FileResponse(out, media_type="video/mp4") + + file_size = out.stat().st_size + range_header = request.headers.get("range") or request.headers.get("Range") + + if range_header: + # Parse "bytes=START-END" + try: + range_str = range_header.replace("bytes=", "").strip() + start_s, end_s = range_str.split("-") + start = int(start_s) if start_s else 0 + end = int(end_s) if end_s else file_size - 1 + end = min(end, file_size - 1) + if start > end or start >= file_size: + return Response(status_code=416) # Range Not Satisfiable + chunk_size = end - start + 1 + + def iter_file(): + with open(out, "rb") as f: + f.seek(start) + remaining = chunk_size + while remaining > 0: + read_size = min(64 * 1024, remaining) + data = f.read(read_size) + if not data: + break + remaining -= len(data) + yield data + + headers = { + "Content-Range": f"bytes {start}-{end}/{file_size}", + "Accept-Ranges": "bytes", + "Content-Length": str(chunk_size), + "Content-Type": "video/mp4", + } + return StreamingResponse(iter_file(), status_code=206, headers=headers, + media_type="video/mp4") + except (ValueError, IndexError): + pass + + # Brez Range — vrni cel file + return FileResponse( + out, + media_type="video/mp4", + headers={"Accept-Ranges": "bytes", "Content-Length": str(file_size)}, + ) @app.delete("/api/jobs/{job_id}") diff --git a/templates/index.html b/templates/index.html index e29fd51..4468738 100644 --- a/templates/index.html +++ b/templates/index.html @@ -56,9 +56,20 @@ display: grid; grid-template-columns: 1fr 1fr; gap: 24px; + align-items: start; + } + main > section.card:first-of-type { + position: sticky; + top: 16px; + max-height: calc(100vh - 32px); + overflow-y: auto; } @media (max-width: 800px) { main { grid-template-columns: 1fr; } + main > section.card:first-of-type { + position: static; + max-height: none; + } } .card { background: var(--panel);