"""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)