"""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 )