"""Subsonic media endpoints: stream, download, cover art. ``stream`` and ``download`` reuse :class:`StreamingService` (honouring HTTP Range) — they return raw bytes, not the Subsonic envelope. Transcoding params (``maxBitRate``/``format``) are accepted but ignored; the original file is served (no in-request ffmpeg — CLAUDE.md). ``getCoverArt`` returns a placeholder until the cover pipeline lands (the ``/api/v1`` cover endpoints are still stubs). """ import base64 from typing import Annotated from fastapi import APIRouter, Header, Query from fastapi.responses import Response, StreamingResponse from app.api.deps import StreamingServiceDep, SubsonicUser, TrackRepoDep from app.api.rest.ids import decode_track, parse from app.domain.errors import NotFoundError router = APIRouter() # 1x1 transparent PNG - a graceful placeholder until cover art is wired up. _PLACEHOLDER_PNG = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC" ) @router.api_route("/stream", methods=["GET", "POST"]) @router.api_route("/stream.view", methods=["GET", "POST"]) async def stream( _user: SubsonicUser, service: StreamingServiceDep, id: Annotated[str, Query()], range_header: Annotated[str | None, Header(alias="Range")] = None, ) -> StreamingResponse: result = await service.open_stream(decode_track(id), range_header) headers = {"Accept-Ranges": "bytes", "Content-Length": str(result.content_length)} status_code = 200 if result.is_partial: headers["Content-Range"] = f"bytes {result.start}-{result.end}/{result.total_size}" status_code = 206 return StreamingResponse( result.stream, status_code=status_code, headers=headers, media_type=result.content_type ) @router.api_route("/download", methods=["GET", "POST"]) @router.api_route("/download.view", methods=["GET", "POST"]) async def download( _user: SubsonicUser, service: StreamingServiceDep, track_repo: TrackRepoDep, id: Annotated[str, Query()], ) -> StreamingResponse: track_id = decode_track(id) track = await track_repo.get_by_id(track_id) if track is None: raise NotFoundError("Song not found.") result = await service.open_stream(track_id, None) filename = f"{track.title}.{track.file_format}" headers = { "Content-Length": str(result.content_length), "Content-Disposition": f'attachment; filename="{filename}"', } return StreamingResponse(result.stream, headers=headers, media_type=result.content_type) @router.api_route("/getCoverArt", methods=["GET", "POST"]) @router.api_route("/getCoverArt.view", methods=["GET", "POST"]) async def get_cover_art( _user: SubsonicUser, id: Annotated[str, Query()], size: Annotated[int | None, Query()] = None, ) -> Response: # Validate the id shape so clients get a clean error on garbage, then serve a # placeholder. TODO: stream real covers once the cover pipeline exists. parse(id) return Response(content=_PLACEHOLDER_PNG, media_type="image/png")