313af3a070
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>
94 lines
2.3 KiB
Python
94 lines
2.3 KiB
Python
"""Domain exception hierarchy — framework-agnostic.
|
|
|
|
Services raise these; the API layer maps them to HTTP responses
|
|
(see ``app.api.errors``). The domain never references HTTP status codes.
|
|
"""
|
|
|
|
|
|
class DomainError(Exception):
|
|
"""Base for all expected, business-meaningful failures.
|
|
|
|
``code`` is a stable, machine-readable identifier returned to clients.
|
|
"""
|
|
|
|
code: str = "domain_error"
|
|
|
|
def __init__(self, message: str | None = None) -> None:
|
|
super().__init__(message or self.__class__.__doc__ or self.code)
|
|
self.message = str(self)
|
|
|
|
|
|
class NotFoundError(DomainError):
|
|
"""Requested resource does not exist."""
|
|
|
|
code = "not_found"
|
|
|
|
|
|
class AlreadyExistsError(DomainError):
|
|
"""Resource conflicts with an existing one (e.g. duplicate)."""
|
|
|
|
code = "already_exists"
|
|
|
|
|
|
class ConflictError(DomainError):
|
|
"""Operation conflicts with current state (e.g. stale version on write)."""
|
|
|
|
code = "conflict"
|
|
|
|
|
|
class ValidationError(DomainError):
|
|
"""Input is well-formed but violates a business rule."""
|
|
|
|
code = "validation_error"
|
|
|
|
|
|
class AuthenticationError(DomainError):
|
|
"""Caller could not be authenticated."""
|
|
|
|
code = "authentication_error"
|
|
|
|
|
|
class PermissionDeniedError(DomainError):
|
|
"""Caller authenticated but not authorized for this action."""
|
|
|
|
code = "permission_denied"
|
|
|
|
|
|
class NotSupportedError(DomainError):
|
|
"""Operation is intentionally unsupported (e.g. a config knob that is managed
|
|
via environment, not mutable at runtime)."""
|
|
|
|
code = "not_supported"
|
|
|
|
|
|
class DependencyUnavailableError(DomainError):
|
|
"""An external dependency (source, ML, MusicBrainz) is unavailable.
|
|
|
|
Callers should degrade gracefully rather than propagate as a hard 500.
|
|
"""
|
|
|
|
code = "dependency_unavailable"
|
|
|
|
|
|
class StorageError(DomainError):
|
|
"""File storage operation failed."""
|
|
|
|
code = "storage_error"
|
|
|
|
|
|
class TranscodeError(DomainError):
|
|
"""Transcoding (ffmpeg) failed. Raised in the worker; a play falls back to
|
|
the original file rather than surfacing this."""
|
|
|
|
code = "transcode_error"
|
|
|
|
|
|
class RangeNotSatisfiableError(DomainError):
|
|
"""Requested byte range cannot be satisfied."""
|
|
|
|
code = "range_not_satisfiable"
|
|
|
|
def __init__(self, total_size: int) -> None:
|
|
super().__init__("Requested range is not satisfiable.")
|
|
self.total_size = total_size
|