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
This commit is contained in:
parent
0ca33be6ac
commit
05fb0081c6
51
app/main.py
51
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}")
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user