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).
 */
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; 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. */
    data object Instrumental : LyricsResult

    /** LRCLIB has no record of this track. */
    data object NotFound : LyricsResult

    /** External calls are disabled in privacy settings. UI surfaces this with a
     *  pointer to the setting rather than pretending we searched and found nothing. */
    data object PrivacyDisabled : LyricsResult

    /** Network / parse / anything else went sideways. */
    data class Error(val message: String) : LyricsResult
}