"""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-`` track, ``al-`` album, ``ar-`` artist, ``pl-`` playlist). Cover-art ids reuse the entity's own id (an album cover is ``al-``, a track cover ``tr-``). 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)