EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

LyricsData.kt

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<LrcLine>,
        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
}