feat(transcode): optimize + on-the-fly quality + HLS streaming (§6.6)

POST /tracks/{id}/optimize enqueues a transcode; GET /stream/{id}?quality=
serves a cached Opus rendition (miss → master + background warm); GET
/stream/{id}/hls/{playlist.m3u8,segment} serves the worker-generated HLS
rendition (AAC-in-TS), with ?token= propagated onto segment URLs for players
that can't set headers. Hexagonal: Transcoder port, FfmpegTranscoder adapter,
TranscodeService + cache-path helpers, transcode_track worker (idempotent),
schemas + deps wiring. ffmpeg already in the image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Цвылев Александр Вадимович
2026-07-28 21:19:36 +03:00
parent 8271de34eb
commit 313af3a070
13 changed files with 384 additions and 11 deletions
+23 -5
View File
@@ -1,7 +1,7 @@
"""Track endpoints."""
import uuid
from typing import Any
from typing import Annotated, Any
from fastapi import APIRouter, Query, Response
from fastapi.responses import StreamingResponse
@@ -30,10 +30,12 @@ from app.api.schemas.track import (
TrackOut,
TrackUpdate,
)
from app.api.schemas.transcode import OptimizeEnqueuedOut
from app.application.transcode_service import bitrate_for_quality
from app.domain.entities.album import Album
from app.domain.entities.track import Artist, Track
from app.domain.errors import NotFoundError
from app.workers.queue import enqueue
from app.domain.errors import NotFoundError, ValidationError
from app.workers.queue import enqueue, enqueue_transcode
router = APIRouter(prefix="/tracks", tags=["tracks"])
@@ -222,8 +224,24 @@ async def delete_track(
async def get_similar_tracks(track_id: uuid.UUID, _: CurrentUser) -> Any: ...
@router.post("/{track_id}/optimize")
async def optimize_track(track_id: uuid.UUID, _: CurrentUser) -> Any: ...
@router.post("/{track_id}/optimize", status_code=202)
async def optimize_track(
track_id: uuid.UUID,
track_repo: TrackRepoDep,
_: CurrentUser,
quality: Annotated[str, Query()] = "high",
) -> OptimizeEnqueuedOut:
"""Enqueue transcoding of a track into a cached Opus rendition + HLS (§6.6).
Heavy ffmpeg work runs in the worker; this only queues it."""
if bitrate_for_quality(quality) is None:
raise ValidationError(
f"Unknown quality '{quality}'; expected one of high, medium, low."
)
track = await track_repo.get_by_id(track_id)
if track is None:
raise NotFoundError(f"Track {track_id} not found.")
job_id = await enqueue_transcode(track_id, quality=quality, hls=True)
return OptimizeEnqueuedOut(status="enqueued", job_id=job_id, quality=quality)
@router.get("/{track_id}/lyrics")