Add offline clip caching
This commit is contained in:
@@ -4,13 +4,14 @@ import secrets
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, Response, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..auth import require_session
|
||||
from ..database import get_db
|
||||
from ..http_cache import build_etag, is_matching_etag, set_no_store, set_private_revalidate, set_public_immutable
|
||||
from ..models import AudioAsset, AudioClip, GameAssignment, UserSession
|
||||
from ..schemas import (
|
||||
AudioAssetImportCreate,
|
||||
@@ -50,6 +51,16 @@ def clip_to_response(clip: AudioClip) -> AudioClipResponse:
|
||||
)
|
||||
|
||||
|
||||
def prepare_conditional_response(
|
||||
request: Request,
|
||||
payload: object,
|
||||
*,
|
||||
exclude_keys: set[str] | None = None,
|
||||
) -> tuple[str, bool]:
|
||||
etag = build_etag(payload, exclude_keys=exclude_keys)
|
||||
return etag, is_matching_etag(request, etag)
|
||||
|
||||
|
||||
def can_manage_asset(session: UserSession, asset: AudioAsset, owner_external_player_id: str | None = None) -> bool:
|
||||
if session.is_admin or asset.uploaded_by_session_id == session.id:
|
||||
return True
|
||||
@@ -211,6 +222,8 @@ def import_audio(
|
||||
|
||||
@router.get("/assets", response_model=list[AudioAssetResponse])
|
||||
def list_assets(
|
||||
request: Request,
|
||||
response: Response,
|
||||
external_team_id: str,
|
||||
owner_external_player_id: str | None = None,
|
||||
_: UserSession = Depends(require_session),
|
||||
@@ -220,7 +233,12 @@ def list_assets(
|
||||
if owner_external_player_id:
|
||||
query = query.where(AudioAsset.owner_external_player_id == owner_external_player_id)
|
||||
assets = db.scalars(query.order_by(AudioAsset.created_at.desc())).all()
|
||||
return [AudioAssetResponse.model_validate(asset, from_attributes=True) for asset in assets]
|
||||
payload = [AudioAssetResponse.model_validate(asset, from_attributes=True) for asset in assets]
|
||||
etag, not_modified = prepare_conditional_response(request, payload)
|
||||
set_private_revalidate(response, etag=etag)
|
||||
if not_modified:
|
||||
return Response(status_code=304, headers=dict(response.headers))
|
||||
return payload
|
||||
|
||||
|
||||
@router.delete("/assets/{asset_id}", status_code=204)
|
||||
@@ -363,6 +381,8 @@ def delete_clip(
|
||||
|
||||
@router.get("/clips", response_model=list[AudioClipResponse])
|
||||
def list_clips(
|
||||
request: Request,
|
||||
response: Response,
|
||||
external_team_id: str,
|
||||
owner_external_player_id: str | None = None,
|
||||
include_hidden: bool = False,
|
||||
@@ -380,7 +400,12 @@ def list_clips(
|
||||
if not include_hidden:
|
||||
query = query.where(AudioClip.hidden.is_(False))
|
||||
clips = db.scalars(query).all()
|
||||
return [clip_to_response(clip) for clip in clips]
|
||||
payload = [clip_to_response(clip) for clip in clips]
|
||||
etag, not_modified = prepare_conditional_response(request, payload)
|
||||
set_private_revalidate(response, etag=etag)
|
||||
if not_modified:
|
||||
return Response(status_code=304, headers=dict(response.headers))
|
||||
return payload
|
||||
|
||||
|
||||
@router.post("/clips/reorder", status_code=204)
|
||||
@@ -415,4 +440,16 @@ def media_file(relative_path: str) -> FileResponse:
|
||||
path = storage.absolute_path(relative_path)
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
return FileResponse(path)
|
||||
stat = path.stat()
|
||||
if path.is_relative_to(storage.normalized_dir):
|
||||
etag = build_etag({"path": str(path.relative_to(storage.root)), "size": stat.st_size, "mtime_ns": stat.st_mtime_ns})
|
||||
response = FileResponse(
|
||||
path,
|
||||
stat_result=stat,
|
||||
)
|
||||
set_public_immutable(response, etag=etag)
|
||||
return response
|
||||
|
||||
response = FileResponse(path, stat_result=stat)
|
||||
set_no_store(response)
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user