feat(subsonic): response envelope, id scheme, and error mapping
- envelope: one serializer emitting the <subsonic-response> wrapper in XML (default) and JSON (f=json), carrying status/version/type/serverVersion - ids: stable, reversible type-prefixed ids (tr-/al-/ar-/pl-) ↔ UUIDs - errors: /rest requests render the Subsonic error envelope (always HTTP 200) with standard codes (10 missing param, 40 wrong creds, 50, 70 not found) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Stable, reversible mapping between Subsonic opaque string ids and our UUIDs.
|
||||
|
||||
Subsonic ids are opaque strings; ours are UUIDs. We use a type-prefixed,
|
||||
human-debuggable convention (``tr-<uuid>`` track, ``al-<uuid>`` album,
|
||||
``ar-<uuid>`` artist, ``pl-<uuid>`` playlist). Cover-art ids reuse the entity's
|
||||
own id (an album cover is ``al-<uuid>``, a track cover ``tr-<uuid>``). Centralize
|
||||
encode/decode here so the convention lives in exactly one place.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from enum import StrEnum
|
||||
|
||||
from app.domain.errors import NotFoundError
|
||||
|
||||
|
||||
class IdKind(StrEnum):
|
||||
TRACK = "tr"
|
||||
ALBUM = "al"
|
||||
ARTIST = "ar"
|
||||
PLAYLIST = "pl"
|
||||
|
||||
|
||||
def encode(kind: IdKind, value: uuid.UUID) -> str:
|
||||
return f"{kind.value}-{value}"
|
||||
|
||||
|
||||
def encode_track(value: uuid.UUID) -> str:
|
||||
return encode(IdKind.TRACK, value)
|
||||
|
||||
|
||||
def encode_album(value: uuid.UUID) -> str:
|
||||
return encode(IdKind.ALBUM, value)
|
||||
|
||||
|
||||
def encode_artist(value: uuid.UUID) -> str:
|
||||
return encode(IdKind.ARTIST, value)
|
||||
|
||||
|
||||
def encode_playlist(value: uuid.UUID) -> str:
|
||||
return encode(IdKind.PLAYLIST, value)
|
||||
|
||||
|
||||
def parse(raw: str) -> tuple[IdKind, uuid.UUID]:
|
||||
"""Decode any prefixed id into its kind + UUID. Raises ``NotFoundError`` on a
|
||||
malformed id (an unknown id is, from the client's view, simply not found)."""
|
||||
prefix, _, rest = raw.partition("-")
|
||||
try:
|
||||
kind = IdKind(prefix)
|
||||
value = uuid.UUID(rest)
|
||||
except ValueError as exc:
|
||||
raise NotFoundError(f"Unknown id {raw!r}.") from exc
|
||||
return kind, value
|
||||
|
||||
|
||||
def _decode_as(raw: str, expected: IdKind) -> uuid.UUID:
|
||||
kind, value = parse(raw)
|
||||
if kind is not expected:
|
||||
raise NotFoundError(f"Expected a {expected.name.lower()} id, got {raw!r}.")
|
||||
return value
|
||||
|
||||
|
||||
def decode_track(raw: str) -> uuid.UUID:
|
||||
return _decode_as(raw, IdKind.TRACK)
|
||||
|
||||
|
||||
def decode_album(raw: str) -> uuid.UUID:
|
||||
return _decode_as(raw, IdKind.ALBUM)
|
||||
|
||||
|
||||
def decode_artist(raw: str) -> uuid.UUID:
|
||||
return _decode_as(raw, IdKind.ARTIST)
|
||||
|
||||
|
||||
def decode_playlist(raw: str) -> uuid.UUID:
|
||||
return _decode_as(raw, IdKind.PLAYLIST)
|
||||
Reference in New Issue
Block a user