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
+2
View File
@@ -14,6 +14,7 @@ from app.workers.tasks.download_task import download_track
from app.workers.tasks.enrich_task import enrich_track
from app.workers.tasks.import_task import scan_local_folder
from app.workers.tasks.materialize_task import materialize_track
from app.workers.tasks.transcode_task import transcode_track
log = get_logger("worker")
@@ -35,6 +36,7 @@ class WorkerSettings:
download_track,
materialize_track,
cleanup_storage,
transcode_track,
]
on_startup = startup
on_shutdown = shutdown
+22
View File
@@ -59,6 +59,28 @@ async def enqueue_materialize(job_id: uuid.UUID) -> None:
log.warning("materialize_enqueue_failed", job_id=str(job_id))
async def enqueue_transcode(
track_id: uuid.UUID, *, quality: str = "high", hls: bool = True
) -> str:
"""Enqueue a transcode job (§6.6). Unlike the best-effort follow-ups below,
this is user/stream-driven, so it surfaces a job id (and a 503 via the caller
if the queue is down) rather than swallowing failures."""
return await enqueue(
"transcode_track", track_id=str(track_id), quality=quality, hls=hls
)
async def enqueue_transcode_quiet(
track_id: uuid.UUID, *, quality: str = "high", hls: bool = False
) -> None:
"""Best-effort transcode enqueue for a streaming cache miss — never blocks or
fails the play (the master is served meanwhile)."""
try:
await enqueue_transcode(track_id, quality=quality, hls=hls)
except DependencyUnavailableError:
log.warning("transcode_enqueue_failed", track_id=str(track_id))
async def enqueue_enrich(track_id: uuid.UUID) -> None:
"""Best-effort enqueue of metadata enrichment for a freshly stored track.
+64
View File
@@ -0,0 +1,64 @@
"""arq task: transcode a track to a cached Opus rendition and/or HLS (§6.6).
Triggered by ``POST /tracks/{id}/optimize`` and by a streaming cache miss. Reads
the master once (via ``storage.as_local_path``) and writes into
``transcode_cache_path``. Idempotent — existing outputs are skipped, so repeated
enqueues (e.g. several plays before the first finishes) are cheap. The DB session
is released before ffmpeg runs; the track entity is a detached value object.
"""
import uuid
from typing import Any
from app.application.transcode_service import (
HLS_BITRATE,
bitrate_for_quality,
hls_dir,
hls_playlist_path,
opus_path,
)
from app.core.config import get_settings
from app.core.logging import get_logger
from app.infrastructure.db import session_scope
from app.infrastructure.db.repositories import SqlAlchemyTrackRepository
from app.infrastructure.storage.provider import get_file_storage
from app.infrastructure.transcode.ffmpeg import FfmpegTranscoder
log = get_logger("worker.transcode")
async def transcode_track(
_ctx: dict[str, Any],
*,
track_id: str,
quality: str = "high",
hls: bool = True,
) -> dict[str, Any]:
settings = get_settings()
tid = uuid.UUID(track_id)
async with session_scope() as session:
track = await SqlAlchemyTrackRepository(session).get_by_id(tid)
if track is None or track.storage_uri is None:
log.warning("transcode_skip_no_file", track_id=track_id)
return {"track_id": track_id, "opus": False, "hls": False}
storage = get_file_storage()
transcoder = FfmpegTranscoder(settings.ffmpeg_path)
root = settings.transcode_cache_path
did_opus = False
did_hls = False
async with storage.as_local_path(track.storage_uri) as src:
bitrate = bitrate_for_quality(quality)
if bitrate is not None:
dest = opus_path(root, tid, bitrate)
if not dest.exists():
await transcoder.to_opus(src, dest, bitrate_kbps=bitrate)
did_opus = True
if hls and not hls_playlist_path(root, tid).exists():
await transcoder.to_hls(src, hls_dir(root, tid), bitrate_kbps=HLS_BITRATE)
did_hls = True
log.info("transcode_done", track_id=track_id, opus=did_opus, hls=did_hls)
return {"track_id": track_id, "opus": did_opus, "hls": did_hls}