8271de34eb
GET /tracks/{id}/lyrics (get-or-fetch, caches found/not_found with a 7-day
miss TTL) and POST /tracks/{id}/lyrics/refetch (force). Hexagonal wiring:
LyricsProvider/LyricsRepository ports, LrclibHttpClient adapter (keyless,
degrades to not_found on error), SqlAlchemyLyricsRepository (upsert on the
existing lyrics table), LyricsService, LyricsOut schema, deps.py factory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
42 lines
1.2 KiB
Python
42 lines
1.2 KiB
Python
"""Lyrics value objects (plan §6.7).
|
|
|
|
``Lyrics`` is the cached row for a track; ``LyricsResult`` is what a provider
|
|
(LRCLIB) returns for a lookup. Both cross the domain boundary — no framework
|
|
imports. Status values mirror ``LyricsStatus`` in the ORM enum ("found" /
|
|
"not_found" / "pending") but are kept as plain strings here so the domain stays
|
|
independent of the persistence layer.
|
|
"""
|
|
|
|
import datetime as dt
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LyricsResult:
|
|
"""A provider hit: synced (timestamped LRC) and/or plain text."""
|
|
|
|
synced: str | None
|
|
plain: str | None
|
|
source: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Lyrics:
|
|
"""Cached lyrics for one track. ``status`` is ``found`` / ``not_found`` /
|
|
``pending``; ``not_found`` is cached too (with a TTL in the service) so a
|
|
track with no lyrics doesn't hammer the provider on every play."""
|
|
|
|
track_id: uuid.UUID
|
|
synced: str | None
|
|
plain: str | None
|
|
source: str | None
|
|
status: str
|
|
fetched_at: dt.datetime
|
|
|
|
@property
|
|
def has_lyrics(self) -> bool:
|
|
return self.status == "found" and (
|
|
self.synced is not None or self.plain is not None
|
|
)
|