Files
yunke/yunke-server/app/api/routers/media.py
T

55 lines
2.0 KiB
Python
Raw Normal View History

2026-08-01 14:13:50 +08:00
"""GET /api/media/{media_hash} — stream a cached media blob (Task 2.8).
Looks up the :class:`app.db.models.Media` row keyed by ``media_hash`` and
streams the local file with the correct ``Content-Type``. HTTP Range requests
are honored (Starlette's :class:`fastapi.responses.FileResponse` handles
``Range`` / ``206 Partial Content`` natively). Returns ``404`` when the row is
absent, has no local file recorded, or the underlying file is missing on disk.
The endpoint reads the absolute ``local_path`` straight off the row (the
downloader stores it absolute), so it does not need to know the
:class:`ObjectStore` layout — only that the file exists where the row points.
"""
from __future__ import annotations
from pathlib import Path
from typing import Annotated
import sqlalchemy as sa
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_session
from app.db.models import Media
from app.scraper.media import guess_mime_type
router = APIRouter(prefix="/api", tags=["media"])
SessionDep = Annotated[AsyncSession, Depends(get_session)]
@router.get("/media/{media_hash}")
async def get_media(media_hash: str, session: SessionDep) -> FileResponse:
"""Stream the cached media blob for ``media_hash``."""
row = (
await session.execute(
sa.select(Media).where(Media.media_hash == media_hash)
)
).scalar_one_or_none()
if row is None or not row.local_path:
raise HTTPException(status_code=404, detail="media not found")
path = Path(row.local_path)
if not path.is_file():
# Row exists but the blob was removed out-of-band — treat as not-found.
raise HTTPException(status_code=404, detail="media file missing")
media_type = (
row.mime_type
or guess_mime_type(path.suffix)
or "application/octet-stream"
)
return FileResponse(path, media_type=media_type, filename=path.name)