from typing import Annotated import asyncpg from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter, HTTPException, Query from fastapi.responses import FileResponse from utils.cache import DAY_HEADERS, IMMUTABLE_HEADERS from utils.files import content_disposition, media_file_name, resolve_mime from utils.read.media import ( get_media, get_media_version, get_media_versions, get_message_media, ) from utils.read.models import MediaVersionView, MediaView from utils.storage import ContentAddressedStorage router = APIRouter(prefix="/api/media", tags=["media"], route_class=DishkaRoute) Download = Annotated[bool, Query()] def _attachment(headers: dict[str, str], file_name: str) -> dict[str, str]: return { **headers, "Content-Disposition": content_disposition(file_name, attachment=True), } @router.get("/{media_id}/meta") async def media_meta(pool: FromDishka[asyncpg.Pool], media_id: int) -> MediaView: media = await get_media(pool, media_id) if media is None: raise HTTPException(status_code=404, detail="media not found") return media @router.get("/versions/{chat_id}/{message_id}") async def message_media_versions( pool: FromDishka[asyncpg.Pool], chat_id: int, message_id: int, account_id: Annotated[int, Query()], ) -> list[MediaVersionView]: return await get_media_versions(pool, account_id, chat_id, message_id) @router.get("/version/{version_id}") async def serve_media_version( pool: FromDishka[asyncpg.Pool], storage: FromDishka[ContentAddressedStorage], version_id: int, download: Download = False, ) -> FileResponse: version = await get_media_version(pool, version_id) if version is None: raise HTTPException(status_code=404, detail="media version not found") headers = IMMUTABLE_HEADERS if download: headers = _attachment( headers, media_file_name(version.kind, version.mime, version_id) ) return FileResponse( storage.url(version.storage_key), media_type=resolve_mime(version.kind, version.mime), headers=headers, ) @router.get("/message/{chat_id}/{message_id}") async def message_media( pool: FromDishka[asyncpg.Pool], chat_id: int, message_id: int, account_id: Annotated[int, Query()], ) -> MediaView: media = await get_message_media(pool, account_id, chat_id, message_id) if media is None: raise HTTPException(status_code=404, detail="media not found") return media @router.get("/{media_id}") async def serve_media( pool: FromDishka[asyncpg.Pool], storage: FromDishka[ContentAddressedStorage], media_id: int, download: Download = False, ) -> FileResponse: media = await get_media(pool, media_id) if media is None: raise HTTPException(status_code=404, detail="media not found") if not media.downloaded or media.storage_key is None: raise HTTPException( status_code=409, detail="media not downloaded; enqueue fetch via POST /api/media/fetch", ) headers = DAY_HEADERS if download: headers = _attachment(headers, media.file_name or f"media_{media_id}") return FileResponse( storage.url(media.storage_key), media_type=resolve_mime(media.kind, media.mime, media.file_name), headers=headers, )