"""Shared cover-art serving helper (presentation). Streams a stored cover image from the :class:`FileStorage` port. Used by the native ``/api/v1`` cover endpoints and the Subsonic ``getCoverArt`` adapter so the streaming/content-type logic lives in one place. """ import uuid from fastapi.responses import StreamingResponse from app.domain.entities.album import Album from app.domain.errors import NotFoundError, StorageError from app.domain.ports import AlbumRepository, FileStorage, TrackRepository _CONTENT_TYPE_BY_EXT: dict[str, str] = { "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp", "gif": "image/gif", } # Covers are immutable for a given album (a new cover means a new key), so let # clients cache aggressively. _CACHE_CONTROL = "public, max-age=86400" def _content_type_for(key: str) -> str: ext = key.rsplit(".", 1)[-1].lower() if "." in key else "" return _CONTENT_TYPE_BY_EXT.get(ext, "application/octet-stream") async def stream_cover(storage: FileStorage, cover_path: str) -> StreamingResponse: """Stream a stored cover by its storage key. Raises ``NotFoundError`` if the object is missing (a dangling ``cover_path`` reads as "no cover").""" try: stream, total = await storage.open_range(cover_path, 0, None) except StorageError as exc: raise NotFoundError("Cover not found.") from exc return StreamingResponse( stream, media_type=_content_type_for(cover_path), headers={"Content-Length": str(total), "Cache-Control": _CACHE_CONTROL}, ) async def resolve_album_for_track( track_repo: TrackRepository, album_repo: AlbumRepository, track_id: uuid.UUID, ) -> Album | None: """The album that owns a track (cover lives on the album), or ``None``.""" track = await track_repo.get_by_id(track_id) if track is None or track.album_id is None: return None return await album_repo.get_by_id(track.album_id)