package dev.stave.lyrics import kotlinx.serialization.Serializable import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds /** * Raw response shape from `https://lrclib.net/api/get`. LRCLIB is generous about which * fields it returns — plain-text-only tracks get `syncedLyrics = null`, instrumentals * get `instrumental = true` with both lyrics fields null, tracks it has no record of * return a 404 which we surface as [LyricsResult.NotFound]. */ @Serializable data class LrclibResponse( val id: Long? = null, val trackName: String? = null, val artistName: String? = null, val albumName: String? = null, val duration: Double? = null, val instrumental: Boolean = false, val plainLyrics: String? = null, val syncedLyrics: String? = null, ) /** * One LRC line = timestamp + text. Timestamp is the start of the line; the line's end * is the next line's start (or the track's duration for the last one). We store as a * Duration rather than raw millis so display code can format with our `toClock()` * helper and math is unambiguous. */ data class LrcLine(val start: Duration, val text: String) { companion object { fun at(millis: Long, text: String) = LrcLine(millis.milliseconds, text) } } /** * Everything the lyrics UI needs for one track. Synced lyrics give us line-level * highlighting during playback; plain lyrics are shown as a static block. Both may * be present (LRCLIB sometimes returns both); if only plain is present we show that. */ sealed interface LyricsResult { /** Still fetching from LRCLIB (or from the on-disk cache). */ data object Loading : LyricsResult /** Track exists in LRCLIB and has lyrics. [synced] is non-empty when the track has * timestamped lyrics; [plain] is the plain-text version (may also be non-null). */ data class Loaded( val synced: List, val plain: String?, ) : LyricsResult /** LRCLIB knows this track but the rightsholder marked it instrumental — no lyrics * will ever be returned. UI should say so clearly rather than pretend it's still * searching. */ data object Instrumental : LyricsResult /** LRCLIB has no record of this track. Happens for obscure titles, misspelled * artists, or anything before LRCLIB's crawl date. */ data object NotFound : LyricsResult /** User has turned off external calls in privacy settings. We never contacted * LRCLIB at all — the UI must say that, not pretend we tried and found nothing. * Toggling the setting back on triggers a fresh fetch via [LyricsViewModel.refresh]. */ data object PrivacyDisabled : LyricsResult /** Network / parse / anything else went sideways. Rare but worth surfacing. */ data class Error(val message: String) : LyricsResult }