feat: Enhance photo and video handling in admin frontend
CI / skip-ci-check (pull_request) Successful in 7s
CI / python-lint (pull_request) Has been cancelled
CI / test-backend (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / secret-scanning (pull_request) Has been cancelled
CI / dependency-scan (pull_request) Has been cancelled
CI / sast-scan (pull_request) Has been cancelled
CI / workflow-summary (pull_request) Has been cancelled
CI / lint-and-type-check (pull_request) Has been cancelled
CI / skip-ci-check (pull_request) Successful in 7s
CI / python-lint (pull_request) Has been cancelled
CI / test-backend (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / secret-scanning (pull_request) Has been cancelled
CI / dependency-scan (pull_request) Has been cancelled
CI / sast-scan (pull_request) Has been cancelled
CI / workflow-summary (pull_request) Has been cancelled
CI / lint-and-type-check (pull_request) Has been cancelled
- Added media_type to PhotoSearchResult interface to distinguish between images and videos. - Updated PhotoViewer component to support video playback, including URL handling and preloading logic. - Modified openPhoto function in Search page to open videos correctly. - Enhanced backend API to serve video files with range request support for better streaming experience. These changes improve the user experience by allowing seamless viewing of both images and videos in the application.
This commit is contained in:
+89
-5
@@ -5,8 +5,8 @@ from __future__ import annotations
|
||||
from datetime import date
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from fastapi.responses import FileResponse, Response, StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.db.session import get_db
|
||||
@@ -296,11 +296,13 @@ def get_video_thumbnail(
|
||||
@router.get("/{video_id}/video")
|
||||
def get_video_file(
|
||||
video_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> FileResponse:
|
||||
"""Serve video file for playback."""
|
||||
):
|
||||
"""Serve video file for playback with range request support."""
|
||||
import os
|
||||
import mimetypes
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
# Verify video exists
|
||||
video = db.query(Photo).filter(
|
||||
@@ -325,7 +327,89 @@ def get_video_file(
|
||||
if not media_type or not media_type.startswith('video/'):
|
||||
media_type = "video/mp4"
|
||||
|
||||
# Use FileResponse with range request support for video streaming
|
||||
file_size = os.path.getsize(video.path)
|
||||
# Get range header - Starlette normalizes headers to lowercase
|
||||
range_header = request.headers.get("range")
|
||||
|
||||
# Debug: Write to file to verify code execution
|
||||
try:
|
||||
with open("/tmp/video_debug.log", "a") as f:
|
||||
all_headers = {k: v for k, v in request.headers.items()}
|
||||
f.write(f"Video {video_id}: range_header={range_header}, all_headers={all_headers}\n")
|
||||
if hasattr(request, 'scope'):
|
||||
scope_headers = request.scope.get("headers", [])
|
||||
f.write(f" scope headers: {scope_headers}\n")
|
||||
f.flush()
|
||||
except Exception as e:
|
||||
with open("/tmp/video_debug.log", "a") as f:
|
||||
f.write(f"Debug write error: {e}\n")
|
||||
f.flush()
|
||||
|
||||
# Also check request scope directly as fallback
|
||||
if not range_header and hasattr(request, 'scope'):
|
||||
scope_headers = request.scope.get("headers", [])
|
||||
for header_name, header_value in scope_headers:
|
||||
if header_name.lower() == b"range":
|
||||
range_header = header_value.decode() if isinstance(header_value, bytes) else header_value
|
||||
with open("/tmp/video_debug.log", "a") as f:
|
||||
f.write(f" Found range in scope: {range_header}\n")
|
||||
f.flush()
|
||||
break
|
||||
|
||||
if range_header:
|
||||
try:
|
||||
# Parse range header: "bytes=start-end"
|
||||
range_match = range_header.replace("bytes=", "").split("-")
|
||||
start_str = range_match[0].strip()
|
||||
end_str = range_match[1].strip() if len(range_match) > 1 and range_match[1] else ""
|
||||
|
||||
start = int(start_str) if start_str else 0
|
||||
end = int(end_str) if end_str else file_size - 1
|
||||
|
||||
# Validate range
|
||||
if start < 0:
|
||||
start = 0
|
||||
if end >= file_size:
|
||||
end = file_size - 1
|
||||
if start > end:
|
||||
return Response(
|
||||
status_code=416,
|
||||
headers={"Content-Range": f"bytes */{file_size}"}
|
||||
)
|
||||
|
||||
# Read the requested chunk
|
||||
chunk_size = end - start + 1
|
||||
|
||||
def generate_chunk():
|
||||
with open(video.path, "rb") as f:
|
||||
f.seek(start)
|
||||
remaining = chunk_size
|
||||
while remaining > 0:
|
||||
chunk = f.read(min(8192, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
remaining -= len(chunk)
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
return StreamingResponse(
|
||||
generate_chunk(),
|
||||
status_code=206,
|
||||
headers={
|
||||
"Content-Range": f"bytes {start}-{end}/{file_size}",
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Length": str(chunk_size),
|
||||
"Content-Type": media_type,
|
||||
"Content-Disposition": "inline",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
media_type=media_type,
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
# If range parsing fails, fall through to serve full file
|
||||
pass
|
||||
|
||||
# No range request or parsing failed - serve full file with range support headers
|
||||
response = FileResponse(
|
||||
video.path,
|
||||
media_type=media_type,
|
||||
|
||||
Reference in New Issue
Block a user