跳转至

apps\player\src\main\java\cn\nfarms\player\data\Models.kt

源码路径:apps\player\src\main\java\cn\nfarms\player\data\Models.kt · 105 行

package cn.nfarms.player.data

import kotlinx.serialization.Serializable

/**
 * Player 的数据模型。
 *
 * 设计取舍:**曲目本身不存进数据库**,只存「用户对它的状态」
 * (收藏、进度、书签)。原因:文件可以删、可以换位置、可以从别的设备同步过来,
 * 而「这首歌我听到 1:23」这件事跟文件在哪没关系。
 * 于是曲目用 [Track.key] 做身份(内容指纹),跨设备也认得出来。
 */
@Serializable
data class Track(
    val key: String,
    /** 显示名;文件丢了也还知道它是什么 */
    val title: String,
    val artist: String = "",
    val album: String = "",
    /** 本地绝对路径;从别的设备同步来的可能没有 */
    val path: String = "",
    val durationMs: Long = 0,
    val sizeBytes: Long = 0,
    /** 收藏 */
    val favorite: Boolean = false,
    /** 上次听到哪儿(毫秒) */
    val positionMs: Long = 0,
    /** 上次听的时间 —— 用来排「最近播放」 */
    val lastPlayedAt: Long = 0,
    /** 是否加密存储(见 [EncryptedMedia]) */
    val encrypted: Boolean = false,
    /** 歌词内嵌(LRC 文本);独立 .lrc 文件在加载时合并进来 */
    val lyrics: String = "",
) {
    val hasProgress: Boolean get() = positionMs > 5_000 && positionMs < durationMs - 3_000
}

@Serializable
data class Playlist(
    val id: String,
    val name: String,
    val keys: List<String> = emptyList(),
    val createdAt: Long = System.currentTimeMillis(),
    val updatedAt: Long = System.currentTimeMillis(),
)

@Serializable
data class Bookmark(
    val id: String,
    val trackKey: String,
    val positionMs: Long,
    val note: String = "",
    val createdAt: Long = System.currentTimeMillis(),
)

@Serializable
data class LibraryState(
    val tracks: List<Track> = emptyList(),
    val playlists: List<Playlist> = emptyList(),
    val bookmarks: List<Bookmark> = emptyList(),
    /** 播放队列(按 key) */
    val queue: List<String> = emptyList(),
    val queueIndex: Int = 0,
    val speed: Float = 1f,
    /** 播放列表同步的「上次同步时刻」 */
    val lastSyncAt: Long = 0,
)

/** 歌词:一行一个时间点。 */
@Serializable
data class LyricLine(val atMs: Long, val text: String)

/**
 * LRC 解析。
 *
 * 支持 `[mm:ss.xx]` 与 `[mm:ss]` 两种写法 —— 后者很常见(老播放器导出的),
 * 只认前者会让一大批歌词文件"解析出来是空的"。
 * `[ti:]` `[ar:]` 这类标签行直接跳过。
 */
object Lrc {
    private val tag = Regex("""^\[(ti|ar|al|by|offset):.*]$""", RegexOption.IGNORE_CASE)
    private val line = Regex("""\[(\d{1,3}):(\d{2})(?:[.:](\d{1,3}))?](.*)$""")

    fun parse(text: String): List<LyricLine> {
        val out = mutableListOf<LyricLine>()
        for (raw in text.lineSequence()) {
            val s = raw.trim()
            if (s.isEmpty() || tag.matches(s)) continue
            val m = line.find(s) ?: continue
            val min = m.groupValues[1].toLongOrNull() ?: continue
            val sec = m.groupValues[2].toLongOrNull() ?: continue
            val fracRaw = m.groupValues[3]
            val frac = when (fracRaw.length) {
                0 -> 0L
                1 -> fracRaw.toLongOrNull()?.times(100) ?: 0
                2 -> fracRaw.toLongOrNull()?.times(10) ?: 0
                else -> fracRaw.take(3).toLongOrNull() ?: 0
            }
            val content = m.groupValues[4].trim()
            out += LyricLine(min * 60_000 + sec * 1_000 + frac, content)
        }
        return out.sortedBy { it.atMs }
    }
}