"""Transcode service + cache-path helpers (Group B / plan §6.6). Cache layout under ``transcode_cache_path``:: {track_id}/opus_{kbps}.opus # direct quality renditions {track_id}/hls/playlist.m3u8 # HLS rendition (AAC-in-TS) {track_id}/hls/seg_000.ts … The request side (streaming router) only *reads* the cache — misses fall back to the original file and enqueue generation. The worker (``transcode_task``) writes it. Path helpers are module-level so both sides agree on locations without one importing the other. """ import re import uuid from pathlib import Path from app.domain.errors import NotFoundError from app.domain.ports import TrackRepository # Stream-quality name (matches the user-settings ``StreamQuality``) → Opus # bitrate. ``original`` is absent: it means "serve the master, no transcode". QUALITY_BITRATE: dict[str, int] = {"high": 128, "medium": 96, "low": 64} # Single HLS rendition bitrate (AAC). One rendition keeps the MVP simple; a # multi-bitrate ladder can come later. HLS_BITRATE = 128 # Only these segment names may be served, guarding the segment route against # path traversal. _SEGMENT_RE = re.compile(r"^seg_\d{3,}\.ts$") def bitrate_for_quality(quality: str) -> int | None: """Opus bitrate for a quality name, or ``None`` for ``original``/unknown.""" return QUALITY_BITRATE.get(quality) def track_cache_dir(root: Path, track_id: uuid.UUID) -> Path: return root / str(track_id) def opus_path(root: Path, track_id: uuid.UUID, bitrate_kbps: int) -> Path: return track_cache_dir(root, track_id) / f"opus_{bitrate_kbps}.opus" def hls_dir(root: Path, track_id: uuid.UUID) -> Path: return track_cache_dir(root, track_id) / "hls" def hls_playlist_path(root: Path, track_id: uuid.UUID) -> Path: return hls_dir(root, track_id) / "playlist.m3u8" def hls_segment_path(root: Path, track_id: uuid.UUID, name: str) -> Path | None: """Resolve a segment file, or ``None`` if the name is not a valid segment.""" if not _SEGMENT_RE.fullmatch(name): return None return hls_dir(root, track_id) / name class TranscodeService: """Request-side cache lookups for transcoded renditions.""" def __init__(self, *, tracks: TrackRepository, cache_root: Path) -> None: self._tracks = tracks self._root = cache_root async def _require_streamable(self, track_id: uuid.UUID) -> None: track = await self._tracks.get_by_id(track_id) if track is None: raise NotFoundError("Track not found.") if track.storage_uri is None: raise NotFoundError("Track is not yet downloaded.") async def resolve_quality_file( self, track_id: uuid.UUID, quality: str ) -> Path | None: """Cached Opus file for ``quality`` if present, else ``None`` (caller falls back to the master and enqueues generation). ``original`` → None.""" bitrate = bitrate_for_quality(quality) if bitrate is None: return None path = opus_path(self._root, track_id, bitrate) return path if path.exists() else None async def hls_playlist(self, track_id: uuid.UUID) -> Path | None: """Cached HLS playlist if generated, else ``None`` (validates the track exists so an unknown id 404s rather than silently missing).""" await self._require_streamable(track_id) path = hls_playlist_path(self._root, track_id) return path if path.exists() else None def hls_segment(self, track_id: uuid.UUID, name: str) -> Path | None: path = hls_segment_path(self._root, track_id, name) if path is None or not path.exists(): return None return path