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