Add offline clip caching

This commit is contained in:
Codex
2026-04-23 13:55:15 -05:00
parent ec2f440c13
commit 51ac5b2060
20 changed files with 554 additions and 27 deletions

View File

@@ -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