fix lyric show and music trans logic
This commit is contained in:
+23
-3
@@ -8,6 +8,7 @@ import com.zhushenwudi.lovelivemusicplayer.domain.Lyric
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.Menu
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
||||
import com.zhushenwudi.lovelivemusicplayer.util.PathResolver
|
||||
import com.zhushenwudi.lovelivemusicplayer.util.joinMediaRelativePath
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@@ -131,12 +132,14 @@ class LibraryRepository(private val db: LlmpDatabase) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新检查本地文件并更新 `Music.existFile` / `Album.existFile`。
|
||||
* 重新检查本地文件并更新 `Music.existFile` / `Album.existFile`,同时修复
|
||||
* 旧版快传写入的专辑单文件名封面路径。
|
||||
* 存储根路径或 SD 路径变更后、无需全量重导入时使用。
|
||||
*/
|
||||
fun rescanExistFiles(paths: PathResolver): Int {
|
||||
var changed = 0
|
||||
val albumHasFile = mutableMapOf<String, Boolean>()
|
||||
val albumCoverPaths = mutableMapOf<String, String>()
|
||||
getAllMusic().forEach { music ->
|
||||
val updated = paths.refreshExistFile(music)
|
||||
if (updated.existFile != music.existFile) {
|
||||
@@ -144,6 +147,9 @@ class LibraryRepository(private val db: LlmpDatabase) {
|
||||
changed++
|
||||
}
|
||||
val albumId = updated.albumId ?: return@forEach
|
||||
joinMediaRelativePath(updated.baseUrl, updated.coverPath)?.let { coverPath ->
|
||||
albumCoverPaths.putIfAbsent(albumId, coverPath)
|
||||
}
|
||||
if (updated.existFile == true) {
|
||||
albumHasFile[albumId] = true
|
||||
} else {
|
||||
@@ -155,8 +161,22 @@ class LibraryRepository(private val db: LlmpDatabase) {
|
||||
getAllAlbums().forEach { album ->
|
||||
val id = album.albumId ?: return@forEach
|
||||
val shouldExist = albumHasFile[id] == true
|
||||
if (album.existFile != shouldExist) {
|
||||
upsertAlbum(album.copy(existFile = shouldExist))
|
||||
val storedCoverPath = album.coverPath?.trim().orEmpty()
|
||||
val repairedCoverPath = if (
|
||||
storedCoverPath.isEmpty() ||
|
||||
(!storedCoverPath.contains('/') && !storedCoverPath.contains('\\'))
|
||||
) {
|
||||
albumCoverPaths[id] ?: album.coverPath
|
||||
} else {
|
||||
album.coverPath
|
||||
}
|
||||
if (album.existFile != shouldExist || album.coverPath != repairedCoverPath) {
|
||||
upsertAlbum(
|
||||
album.copy(
|
||||
coverPath = repairedCoverPath,
|
||||
existFile = shouldExist,
|
||||
),
|
||||
)
|
||||
changed++
|
||||
}
|
||||
}
|
||||
|
||||
+39
-14
@@ -6,8 +6,8 @@ data class LyricLine(
|
||||
)
|
||||
|
||||
object LrcParser {
|
||||
private val lineRegex =
|
||||
Regex("""\[(\d{1,2}):(\d{1,2})(?:[.:](\d{1,3}))?](.*)""")
|
||||
private val timeTagRegex =
|
||||
Regex("""\[(\d{1,2}):(\d{1,2})(?:[.:](\d{1,3}))?]""")
|
||||
|
||||
fun parse(lrc: String?): List<LyricLine> {
|
||||
if (lrc.isNullOrBlank()) return emptyList()
|
||||
@@ -15,23 +15,48 @@ object LrcParser {
|
||||
for (raw in lrc.lineSequence()) {
|
||||
val trimmed = raw.trim()
|
||||
if (trimmed.isEmpty()) continue
|
||||
val match = lineRegex.find(trimmed) ?: continue
|
||||
val min = match.groupValues[1].toLongOrNull() ?: continue
|
||||
val sec = match.groupValues[2].toLongOrNull() ?: continue
|
||||
val frac = match.groupValues[3]
|
||||
val millis = when {
|
||||
frac.isEmpty() -> 0L
|
||||
frac.length == 1 -> frac.toLong() * 100
|
||||
frac.length == 2 -> frac.toLong() * 10
|
||||
else -> frac.take(3).padEnd(3, '0').toLong()
|
||||
val matches = timeTagRegex.findAll(trimmed).toList()
|
||||
if (matches.isEmpty()) continue
|
||||
val text = trimmed.substring(matches.last().range.last + 1).trim()
|
||||
for (match in matches) {
|
||||
val min = match.groupValues[1].toLongOrNull() ?: continue
|
||||
val sec = match.groupValues[2].toLongOrNull() ?: continue
|
||||
val frac = match.groupValues[3]
|
||||
val millis = when {
|
||||
frac.isEmpty() -> 0L
|
||||
frac.length == 1 -> frac.toLong() * 100
|
||||
frac.length == 2 -> frac.toLong() * 10
|
||||
else -> frac.take(3).padEnd(3, '0').toLong()
|
||||
}
|
||||
lines += LyricLine(timeMs = min * 60_000 + sec * 1_000 + millis, text = text)
|
||||
}
|
||||
val text = match.groupValues[4].trim()
|
||||
if (text.isEmpty()) continue
|
||||
lines += LyricLine(timeMs = min * 60_000 + sec * 1_000 + millis, text = text)
|
||||
}
|
||||
return lines.sortedBy { it.timeMs }
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 LRC 时间标签把扩展歌词对齐到原文;缺失标签保留空行,避免后续歌词整体错位。
|
||||
* 同一时间存在多行时,按各自在源文件中的出现顺序一一配对。
|
||||
*/
|
||||
fun alignByTime(
|
||||
main: List<LyricLine>,
|
||||
extension: List<LyricLine>,
|
||||
): List<LyricLine> {
|
||||
val extensionByTime = extension.groupBy { it.timeMs }
|
||||
val usedAtTime = mutableMapOf<Long, Int>()
|
||||
return main.map { mainLine ->
|
||||
val occurrence = usedAtTime[mainLine.timeMs] ?: 0
|
||||
usedAtTime[mainLine.timeMs] = occurrence + 1
|
||||
LyricLine(
|
||||
timeMs = mainLine.timeMs,
|
||||
text = extensionByTime[mainLine.timeMs]
|
||||
?.getOrNull(occurrence)
|
||||
?.text
|
||||
.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun currentIndex(lines: List<LyricLine>, positionMs: Long): Int {
|
||||
if (lines.isEmpty()) return -1
|
||||
var idx = -1
|
||||
|
||||
+11
-2
@@ -154,9 +154,11 @@ class LyricService(
|
||||
translation = lyric.roma
|
||||
}
|
||||
}
|
||||
val mainLines = LrcParser.parse(main?.takeIf { it.isNotBlank() })
|
||||
val translationLines = LrcParser.parse(translation?.takeIf { it.isNotBlank() })
|
||||
return LoadedLyric(
|
||||
main = LrcParser.parse(main?.takeIf { it.isNotBlank() }),
|
||||
translation = LrcParser.parse(translation?.takeIf { it.isNotBlank() }),
|
||||
main = mainLines,
|
||||
translation = LrcParser.alignByTime(mainLines, translationLines),
|
||||
raw = lyric,
|
||||
)
|
||||
}
|
||||
@@ -168,6 +170,13 @@ data class LoadedLyric(
|
||||
val raw: Lyric? = null,
|
||||
)
|
||||
|
||||
/** 始终按原文时间标签取译文,避免调用方重新退化为下标配对。 */
|
||||
fun LoadedLyric.translationAt(mainIndex: Int): LyricLine? {
|
||||
val mainLine = main.getOrNull(mainIndex) ?: return null
|
||||
return translation.getOrNull(mainIndex)?.takeIf { it.timeMs == mainLine.timeMs }
|
||||
?: translation.firstOrNull { it.timeMs == mainLine.timeMs }
|
||||
}
|
||||
|
||||
data class LyricFetchResult(
|
||||
val loaded: LoadedLyric,
|
||||
val ok: Boolean,
|
||||
|
||||
+9
-2
@@ -26,6 +26,7 @@ import com.zhushenwudi.lovelivemusicplayer.protocol.ProtocolCmd
|
||||
import com.zhushenwudi.lovelivemusicplayer.protocol.TransferChannel
|
||||
import com.zhushenwudi.lovelivemusicplayer.util.AudioPathUtils
|
||||
import com.zhushenwudi.lovelivemusicplayer.util.PathResolver
|
||||
import com.zhushenwudi.lovelivemusicplayer.util.joinMediaRelativePath
|
||||
|
||||
data class TransferUiState(
|
||||
val host: String = "",
|
||||
@@ -36,6 +37,7 @@ data class TransferUiState(
|
||||
val current: String = "",
|
||||
val artist: String = "",
|
||||
val coverUrl: String = "",
|
||||
val coverKey: String = "",
|
||||
val progress: Int = 0,
|
||||
val total: Int = 0,
|
||||
val error: String? = null,
|
||||
@@ -174,6 +176,7 @@ class MusicTransferService(
|
||||
current = item.musicName,
|
||||
artist = item.artist,
|
||||
coverUrl = coverHttpUrl(item),
|
||||
coverKey = coverKey(item),
|
||||
status = I18n.t("downloading_song", item.musicName),
|
||||
)
|
||||
}
|
||||
@@ -248,7 +251,10 @@ class MusicTransferService(
|
||||
}
|
||||
|
||||
private fun coverHttpUrl(item: DownloadMusic): String =
|
||||
fileHttpUrl(item.baseUrl, item.coverPath)
|
||||
if (item.coverPath.isBlank()) "" else fileHttpUrl(item.baseUrl, item.coverPath)
|
||||
|
||||
private fun coverKey(item: DownloadMusic): String =
|
||||
joinMediaRelativePath(item.baseUrl, item.coverPath).orEmpty()
|
||||
|
||||
private fun importOne(item: DownloadMusic) {
|
||||
library.upsertAlbum(
|
||||
@@ -256,7 +262,8 @@ class MusicTransferService(
|
||||
albumId = item.albumUId,
|
||||
albumName = item.albumName,
|
||||
date = item.date,
|
||||
coverPath = item.coverPath,
|
||||
// Album 没有 baseUrl 字段,必须保存可独立解析的完整相对路径。
|
||||
coverPath = coverKey(item).ifBlank { null },
|
||||
category = item.category,
|
||||
group = item.group,
|
||||
existFile = true,
|
||||
|
||||
+17
-4
@@ -8,6 +8,19 @@ import com.zhushenwudi.lovelivemusicplayer.platform.AppLog
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.FileSystem
|
||||
import com.zhushenwudi.lovelivemusicplayer.settings.AppSettings
|
||||
|
||||
/**
|
||||
* 将歌曲的目录和文件相对路径合并成专辑可独立使用的完整相对路径。
|
||||
* 已经是完整路径时不重复拼接,兼容云端导入和旧数据库数据。
|
||||
*/
|
||||
internal fun joinMediaRelativePath(baseUrl: String?, path: String?): String? {
|
||||
val child = path?.trim()?.trim('/', '\\').orEmpty()
|
||||
if (child.isEmpty()) return null
|
||||
val base = baseUrl?.trim()?.trim('/', '\\').orEmpty()
|
||||
if (base.isEmpty()) return child
|
||||
if (child == base || child.startsWith("$base/")) return child
|
||||
return "$base/$child"
|
||||
}
|
||||
|
||||
class PathResolver(
|
||||
private val fs: FileSystem,
|
||||
private val settings: AppSettings,
|
||||
@@ -56,9 +69,8 @@ class PathResolver(
|
||||
}
|
||||
|
||||
fun absoluteCoverPath(music: Music): String? {
|
||||
val base = music.baseUrl ?: return null
|
||||
val path = music.coverPath ?: return null
|
||||
return storageRoot() + base.trimStart('/') + path.trimStart('/')
|
||||
val relative = joinMediaRelativePath(music.baseUrl, music.coverPath) ?: return null
|
||||
return storageRoot() + relative
|
||||
}
|
||||
|
||||
fun absoluteAlbumCoverPath(album: Album): String? {
|
||||
@@ -93,7 +105,8 @@ class PathResolver(
|
||||
}
|
||||
}
|
||||
if (settings.enableHttp) {
|
||||
return joinHttpUrl(music.baseUrl.orEmpty(), music.coverPath.orEmpty())
|
||||
val relative = joinMediaRelativePath(music.baseUrl, music.coverPath) ?: return null
|
||||
return joinHttpUrl(relative)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user