-
-
Notifications
You must be signed in to change notification settings - Fork 606
feat(lyrics): major lyrics improvement #3630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d0e5742
b4e4b3e
3e7ce49
3738090
563871f
7981187
c42cc75
ae9fd71
c32682b
dc2c51d
9e6e25c
3bf0667
83fd82a
db0fff8
30ac8d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -270,11 +270,12 @@ object LyricsPlusProvider : LyricsProvider { | |
| } | ||
| } ?: return null | ||
|
|
||
| val parsedLines = runCatching { TTMLParser.parseTTML(ttml) } | ||
| val agentsMap = mutableMapOf<String, TTMLParser.TTMLAgent>() | ||
| val parsedLines = runCatching { TTMLParser.parseTTML(ttml, agentsMap) } | ||
| .onFailure { Timber.tag("LyricsPlus").w(it, "Failed parsing binimum TTML") } | ||
| .getOrNull() | ||
| ?.takeIf { it.isNotEmpty() } ?: return null | ||
| val lrc = runCatching { TTMLParser.toLRC(parsedLines).trim() } | ||
| val lrc = runCatching { TTMLParser.toLRC(parsedLines, agentsMap).trim() } | ||
| .getOrNull() | ||
| ?.takeIf { it.isNotBlank() } | ||
| ?: return null | ||
|
|
@@ -296,6 +297,11 @@ object LyricsPlusProvider : LyricsProvider { | |
| */ | ||
| private fun convertToLrc(response: LyricsPlusResponse?): String? { | ||
| val lyrics = response?.lyrics?.takeIf { it.isNotEmpty() } ?: return null | ||
|
|
||
| if (response.type.equals("None", ignoreCase = true)) { | ||
| return lyrics.joinToString("\n") { it.text.trim() }.ifBlank { null } | ||
| } | ||
|
|
||
| val isWordSync = response.type.equals("Word", ignoreCase = true) | ||
|
|
||
| // Agent mapping | ||
|
|
@@ -305,19 +311,17 @@ object LyricsPlusProvider : LyricsProvider { | |
| lyrics.forEach { line -> | ||
| val raw = line.element?.singer?.lowercase() ?: return@forEach | ||
| if (raw !in agentMap) { | ||
| agentMap[raw] = when { | ||
| raw == "v1" || raw == "v2" || raw == "v1000" -> raw | ||
| else -> { | ||
| val taken = agentMap.values.toSet() | ||
| listOf("v1", "v2").firstOrNull { it !in taken } ?: "v1" | ||
| } | ||
| } | ||
| agentMap[raw] = raw | ||
| } | ||
| } | ||
| val isMultiAgent = agentMap.size > 1 || | ||
| (agentMap.size == 1 && !agentMap.containsKey("v1")) | ||
|
|
||
| val sb = StringBuilder(lyrics.size * 128) | ||
|
|
||
| // Agent metadata header | ||
| response.metadata?.agents?.forEach { (_, info) -> | ||
| val alias = info.alias?.takeIf { it.isNotBlank() } ?: return@forEach | ||
| sb.append("[agent:$alias:${info.type ?: "person"}:${info.name ?: ""}]\n") | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| var lastWasBg = false | ||
|
|
||
| for (line in lyrics) { | ||
|
|
@@ -337,8 +341,8 @@ object LyricsPlusProvider : LyricsProvider { | |
| if (mainText.isNotBlank()) { | ||
| lastWasBg = false | ||
| val agentId = agentMap[line.element?.singer?.lowercase()] | ||
| val agentTag = if (isMultiAgent && agentId != null) "{agent:$agentId}" else "" | ||
| sb.appendLrcLine(line.time, agentTag, mainText) | ||
| val agentTag = if (!agentId.isNullOrBlank()) "{agent:$agentId}" else "" | ||
| sb.appendLrcLine(line.time, line.duration, agentTag, mainText) | ||
| if (isWordSync && mainWords.isNotEmpty()) sb.appendWordBlock(mainWords) | ||
| } | ||
|
|
||
|
|
@@ -352,7 +356,12 @@ object LyricsPlusProvider : LyricsProvider { | |
| if (bgText.isNotBlank()) { | ||
| val bgTime = bgToEmit.minOf { it.time } | ||
| val bgTag = if (lastWasBg) "" else "{bg}" | ||
| sb.appendLrcLine(bgTime, bgTag, bgText) | ||
| val bgDuration = bgToEmit | ||
| .maxOf { it.time + it.duration } | ||
| .minus(bgTime) | ||
| .takeIf { it > 0 } | ||
| ?: line.duration | ||
| sb.appendLrcLine(bgTime, bgDuration, bgTag, bgText) | ||
| lastWasBg = true | ||
| if (isWordSync) sb.appendWordBlock(bgToEmit) | ||
| } | ||
|
|
@@ -367,10 +376,13 @@ object LyricsPlusProvider : LyricsProvider { | |
| words.joinToString("") { it.text }.trim() | ||
|
|
||
| /** Appends `[mm:ss.cc]<tag>text\n` */ | ||
| private fun StringBuilder.appendLrcLine(timeMs: Long, tag: String, text: String) { | ||
| private fun StringBuilder.appendLrcLine(timeMs: Long, durationMs: Long, tag: String, text: String) { | ||
| append(formatLrcTime(timeMs)) | ||
| append(tag) | ||
| append(text) | ||
| if (durationMs > 0) { | ||
| append(formatLrcTime(timeMs + durationMs)) | ||
| } | ||
| append('\n') | ||
| } | ||
|
Comment on lines
378
to
387
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate the LyricsPlusProvider.kt file
find . -name "LyricsPlusProvider.kt" -type fRepository: MetrolistGroup/Metrolist Length of output: 138 🏁 Script executed: # Once located, read the file around lines 372-381 and context
# Get a sense of the file size first
find . -name "LyricsPlusProvider.kt" | xargs wc -lRepository: MetrolistGroup/Metrolist Length of output: 142 🏁 Script executed: # Read the appendLrcLine function and surrounding context
sed -n '350,390p' ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsPlusProvider.ktRepository: MetrolistGroup/Metrolist Length of output: 1682 🏁 Script executed: # Also check lines 340-365 to see the word-sync usage context
sed -n '340,365p' ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsPlusProvider.ktRepository: MetrolistGroup/Metrolist Length of output: 1238 🏁 Script executed: # Check the KDoc on line 372
sed -n '372,375p' ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsPlusProvider.ktRepository: MetrolistGroup/Metrolist Length of output: 273 🏁 Script executed: # Find TTMLParser.kt to verify the claim about omitting trailing time
find . -name "TTMLParser.kt" -type fRepository: MetrolistGroup/Metrolist Length of output: 145 🏁 Script executed: # Search for the toLRC method in TTMLParser
grep -n "fun toLRC" ./betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/TTMLParser.ktRepository: MetrolistGroup/Metrolist Length of output: 165 🏁 Script executed: # Check the specific code in TTMLParser.toLRC regarding word-sync and trailing timestamps
sed -n '420,480p' ./betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/TTMLParser.ktRepository: MetrolistGroup/Metrolist Length of output: 50 🏁 Script executed: # Read the toLRC method implementation
sed -n '321,380p' ./betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/TTMLParser.ktRepository: MetrolistGroup/Metrolist Length of output: 2427 🏁 Script executed: # Get more context - check file size
wc -l ./betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/TTMLParser.ktRepository: MetrolistGroup/Metrolist Length of output: 149 🏁 Script executed: # Search for LyricsEntry and LyricsViewModel to find the extended-LRC parser
find . -name "LyricsEntry.kt" -o -name "LyricsViewModel.kt" | head -5Repository: MetrolistGroup/Metrolist Length of output: 203 🏁 Script executed: # Search for extended LRC parsing or word block parsing
rg -l "word.*sync|<word:|extended.*lrc" --type kt | head -10Repository: MetrolistGroup/Metrolist Length of output: 94 🏁 Script executed: # Search for LRC parsing logic in LyricsEntry and LyricsViewModel
rg "word\:|<word|parseL|lrc.*pars" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsEntry.kt ./app/src/main/kotlin/com/metrolist/music/viewmodels/LyricsViewModel.kt -A 3Repository: MetrolistGroup/Metrolist Length of output: 167 🏁 Script executed: # Get file sizes to understand scope
wc -l ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsEntry.kt ./app/src/main/kotlin/com/metrolist/music/viewmodels/LyricsViewModel.ktRepository: MetrolistGroup/Metrolist Length of output: 224 🏁 Script executed: # Search for word-sync or extended-LRC parsing with simpler patterns
rg "word|<.*>|parse" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsEntry.kt -A 2 -B 2Repository: MetrolistGroup/Metrolist Length of output: 571 🏁 Script executed: # Also check LyricsViewModel
rg "word|<.*>|parse" ./app/src/main/kotlin/com/metrolist/music/viewmodels/LyricsViewModel.kt -A 2 -B 2Repository: MetrolistGroup/Metrolist Length of output: 1669 🏁 Script executed: # Find LyricsUtils class
find . -name "LyricsUtils.kt" -o -name "LyricsUtils.java"Repository: MetrolistGroup/Metrolist Length of output: 131 🏁 Script executed: # Search for parseLyrics function
rg "fun parseLyrics" --type-listRepository: MetrolistGroup/Metrolist Length of output: 457 🏁 Script executed: # Read LyricsUtils.kt to find parseLyrics
wc -l ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.ktRepository: MetrolistGroup/Metrolist Length of output: 136 🏁 Script executed: # Search for parseLyrics function
rg "fun parseLyrics" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt -A 50Repository: MetrolistGroup/Metrolist Length of output: 2108 🏁 Script executed: # Continue reading parseLyrics to see word block parsing
rg "fun parseLyrics" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt -A 100 | head -120Repository: MetrolistGroup/Metrolist Length of output: 3838 🏁 Script executed: # Search for word block parsing logic (looking for <word patterns)
rg "RICH_SYNC|\\<word|WordTimestamp" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt -B 2 -A 2Repository: MetrolistGroup/Metrolist Length of output: 174 🏁 Script executed: # Search for word block parsing logic - fix regex escaping
rg "RICH_SYNC|word|WordTimestamp" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt -B 2 -A 2 | head -80Repository: MetrolistGroup/Metrolist Length of output: 3853 🏁 Script executed: # Search for parseRichSyncLyrics to see how word blocks are handled
rg "fun parseRichSyncLyrics" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt -A 80Repository: MetrolistGroup/Metrolist Length of output: 4712 🏁 Script executed: # Search for parseStandardLyrics to see how standard LRC format is parsed
rg "fun parseStandardLyrics" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt -A 60Repository: MetrolistGroup/Metrolist Length of output: 2624 🏁 Script executed: # Also search for TRAILING_TIME_REGEX definition
rg "TRAILING_TIME_REGEX" ./app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.ktRepository: MetrolistGroup/Metrolist Length of output: 856 Update
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import com.github.promeg.pinyinhelper.Pinyin | |
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.withContext | ||
| import java.util.Locale | ||
| import com.metrolist.music.ui.screens.settings.LyricsPosition | ||
|
|
||
| val LINE_REGEX = "((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\] ?)+)(.*)".toRegex() | ||
| val TIME_REGEX = "\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]".toRegex() | ||
|
|
@@ -29,8 +30,18 @@ private val PAXSENIX_BG_LINE_REGEX = "^\\[bg:\\s*(.*)\\]$".toRegex() | |
| private val AGENT_REGEX = "\\{agent:([^}]+)\\}".toRegex() | ||
| private val BACKGROUND_REGEX = "^\\{bg\\}".toRegex() | ||
|
|
||
| // Regex for trailing line-level end time or duration (supports [mm:ss.cc] and <mm:ss.cc>) | ||
| private val TRAILING_TIME_REGEX = "[<\\[](\\d{1,2}):(\\d{2})\\.(\\d{2,3})[>\\]]\\s*$".toRegex() | ||
|
|
||
| private val AGENT_HEADER_REGEX = "\\[agent:([^:]+):([^:]*):?([^]]*)\\]".toRegex() | ||
|
|
||
| @Suppress("RegExpRedundantEscape") | ||
| object LyricsUtils { | ||
| private data class LyricsAgentMetadata( | ||
| val id: String, | ||
| val type: String = "person", | ||
| val name: String = "" | ||
| ) | ||
| fun cleanTitleForSearch(title: String): String { | ||
| return title.replace(Regex("\\s*[(\\[].*?[)\\]]"), "").trim() | ||
| } | ||
|
|
@@ -436,23 +447,92 @@ object LyricsUtils { | |
|
|
||
| val decodedLyrics = decodeHtmlEntities(unescapedLyrics) | ||
|
|
||
| val lines = decodedLyrics.lines() | ||
| .filter { | ||
| it.isNotBlank() || it.trim().startsWith("[") || it.trim().startsWith("<") | ||
| val rawLines = decodedLyrics.lines() | ||
| val agents = mutableMapOf<String, LyricsAgentMetadata>() | ||
| val lines = rawLines.filter { line -> | ||
| val trimmed = line.trim() | ||
| val agentMatch = AGENT_HEADER_REGEX.find(trimmed) | ||
| if (agentMatch != null) { | ||
| val alias = agentMatch.groupValues[1] | ||
| val type = agentMatch.groupValues[2].ifEmpty { "person" } | ||
| val name = agentMatch.groupValues[3] | ||
| agents[alias] = LyricsAgentMetadata(alias, type, name) | ||
| false | ||
|
Comment on lines
+452
to
+460
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only strip full agent-header lines. Using Proposed fix- val agentMatch = AGENT_HEADER_REGEX.find(trimmed)
+ val agentMatch = AGENT_HEADER_REGEX.matchEntire(trimmed)🤖 Prompt for AI Agents |
||
| } else { | ||
| trimmed.isNotBlank() || trimmed.startsWith("[") || trimmed.startsWith("<") | ||
| } | ||
| .filter { !it.trim().startsWith("[offset:") } | ||
| }.filter { !it.trim().startsWith("[offset:") } | ||
|
|
||
| // Check if this is rich sync format (contains <MM:SS.mm> patterns) | ||
| val isRichSync = lines.any { line -> | ||
| RICH_SYNC_LINE_REGEX.matches(line.trim()) && | ||
| RICH_SYNC_WORD_REGEX.containsMatchIn(line) | ||
| } | ||
|
|
||
| return if (isRichSync) { | ||
| val parsed = if (isRichSync) { | ||
| parseRichSyncLyrics(lines) | ||
| } else { | ||
| parseStandardLyrics(lines) | ||
| } | ||
|
|
||
| applyAgentPositioning(parsed, agents) | ||
| return parsed | ||
| } | ||
|
|
||
| private fun applyAgentPositioning(entries: List<LyricsEntry>, agentMetadata: Map<String, LyricsAgentMetadata>) { | ||
| if (entries.isEmpty()) return | ||
|
|
||
| var currentSideIsLeft = true | ||
| var lastPersonSingerId: String? = null | ||
| var rightCount = 0 | ||
| var totalCount = 0 | ||
|
|
||
| entries.forEach { entry -> | ||
| val singerId = entry.agent | ||
| if (singerId != null) { | ||
| val agentData = agentMetadata[singerId] | ||
| val type = agentData?.type ?: when (singerId) { | ||
| "v1000" -> "group" | ||
| "v2000" -> "other" | ||
| else -> "person" | ||
| } | ||
|
|
||
| if (type == "group") { | ||
| entry.linePosition = LyricsPosition.CENTER | ||
| } else { | ||
| if (lastPersonSingerId == null) { | ||
| currentSideIsLeft = type != "other" | ||
| } else if (singerId != lastPersonSingerId) { | ||
| currentSideIsLeft = !currentSideIsLeft | ||
| } | ||
|
|
||
| entry.linePosition = if (currentSideIsLeft) { | ||
| LyricsPosition.LEFT | ||
| } else { | ||
| LyricsPosition.RIGHT | ||
| } | ||
|
|
||
| if (entry.linePosition == LyricsPosition.RIGHT) { | ||
| rightCount++ | ||
| } | ||
| totalCount++ | ||
| lastPersonSingerId = singerId | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 85% flip logic: if >= 85% of lines are on the right, flip all sides | ||
| if (totalCount > 0 && (rightCount.toDouble() / totalCount.toDouble()) >= 0.85) { | ||
| entries.forEach { entry -> | ||
| when (entry.linePosition) { | ||
| LyricsPosition.LEFT -> | ||
| entry.linePosition = LyricsPosition.RIGHT | ||
| LyricsPosition.RIGHT -> | ||
| entry.linePosition = LyricsPosition.LEFT | ||
| else -> {} | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -480,11 +560,22 @@ object LyricsUtils { | |
| } else null | ||
| } | ||
|
|
||
| val endTimeMatch = TRAILING_TIME_REGEX.find(content) | ||
| val lineEndTimeMs = endTimeMatch?.let { match -> | ||
| val min = match.groupValues[1].toLong() | ||
| val sec = match.groupValues[2].toLong() | ||
| val milString = match.groupValues[3] | ||
| var mil = milString.toLong() | ||
| if (milString.length == 2) mil *= 10 | ||
| min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil | ||
| } | ||
| val contentToClean = if (endTimeMatch != null) content.replaceFirst(TRAILING_TIME_REGEX, "") else content | ||
|
|
||
| // Extract plain text (remove all <MM:SS.mm> tags) | ||
| val plainText = content.replace(Regex("<\\d{1,2}:\\d{2}\\.\\d{2,3}>\\s*"), "").trim() | ||
| val plainText = contentToClean.replace(Regex("<\\d{1,2}:\\d{2}\\.\\d{2,3}>\\s*"), "").trim() | ||
|
|
||
| val lineTimeMs = wordTimings?.firstOrNull()?.startTime?.let { (it * 1000).toLong() } ?: 0L | ||
| result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = lastNonBgAgent ?: "bg", isBackground = true)) | ||
| result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = lastNonBgAgent ?: "bg", isBackground = true, endTime = lineEndTimeMs)) | ||
| return@forEachIndexed | ||
| } | ||
|
|
||
|
|
@@ -509,13 +600,24 @@ object LyricsUtils { | |
| } else null | ||
| } | ||
|
|
||
| val endTimeMatch = TRAILING_TIME_REGEX.find(content) | ||
| val lineEndTimeMs = endTimeMatch?.let { match -> | ||
| val min = match.groupValues[1].toLong() | ||
| val sec = match.groupValues[2].toLong() | ||
| val milString = match.groupValues[3] | ||
| var mil = milString.toLong() | ||
| if (milString.length == 2) mil *= 10 | ||
| min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil | ||
| } | ||
| val contentToClean = if (endTimeMatch != null) content.replaceFirst(TRAILING_TIME_REGEX, "") else content | ||
|
|
||
| // Extract plain text (remove all <MM:SS.mm> tags) | ||
| val plainText = content.replace(Regex("<\\d{1,2}:\\d{2}\\.\\d{2,3}>\\s*"), "").trim() | ||
| val plainText = contentToClean.replace(Regex("<\\d{1,2}:\\d{2}\\.\\d{2,3}>\\s*"), "").trim() | ||
|
|
||
| if (!agent.isNullOrBlank()) { | ||
| lastNonBgAgent = agent | ||
| } | ||
| result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = agent, isBackground = false)) | ||
| result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = agent, isBackground = false, endTime = lineEndTimeMs)) | ||
| return@forEachIndexed | ||
| } | ||
|
|
||
|
|
@@ -554,13 +656,24 @@ object LyricsUtils { | |
| } else null | ||
| } | ||
|
|
||
| val endTimeMatch = TRAILING_TIME_REGEX.find(content) | ||
| val lineEndTimeMs = endTimeMatch?.let { match -> | ||
| val min = match.groupValues[1].toLong() | ||
| val sec = match.groupValues[2].toLong() | ||
| val milString = match.groupValues[3] | ||
| var mil = milString.toLong() | ||
| if (milString.length == 2) mil *= 10 | ||
| min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil | ||
| } | ||
| val contentToClean = if (endTimeMatch != null) content.replaceFirst(TRAILING_TIME_REGEX, "") else content | ||
|
|
||
| // Extract plain text (remove all <MM:SS.mm> tags) | ||
| val plainText = content.replace(Regex("<\\d{1,2}:\\d{2}\\.\\d{2,3}>\\s*"), "").trim() | ||
| val plainText = contentToClean.replace(Regex("<\\d{1,2}:\\d{2}\\.\\d{2,3}>\\s*"), "").trim() | ||
|
|
||
| if (!isBackground && !agent.isNullOrBlank()) { | ||
| lastNonBgAgent = agent | ||
| } | ||
| result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = if (isBackground) lastNonBgAgent ?: "bg" else agent, isBackground = isBackground)) | ||
| result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = if (isBackground) lastNonBgAgent ?: "bg" else agent, isBackground = isBackground, endTime = lineEndTimeMs)) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -769,6 +882,19 @@ object LyricsUtils { | |
| text = text.replaceFirst(BACKGROUND_REGEX, "") | ||
| } | ||
|
|
||
| val endTimeMatch = TRAILING_TIME_REGEX.find(text) | ||
| var parsedEndTime: Long? = endTimeMatch?.let { match -> | ||
| val min = match.groupValues[1].toLong() | ||
| val sec = match.groupValues[2].toLong() | ||
| val milString = match.groupValues[3] | ||
| var mil = milString.toLong() | ||
| if (milString.length == 2) mil *= 10 | ||
| min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil | ||
| } | ||
| if (endTimeMatch != null) { | ||
| text = text.replaceFirst(TRAILING_TIME_REGEX, "") | ||
| } | ||
|
|
||
| return timeMatchResults | ||
| .map { timeMatchResult -> | ||
| val min = timeMatchResult.groupValues[1].toLong() | ||
|
|
@@ -779,7 +905,7 @@ object LyricsUtils { | |
| mil *= 10 | ||
| } | ||
| val time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil | ||
| LyricsEntry(time, text, words, agent = agent, isBackground = isBackground) | ||
| LyricsEntry(time, text, words, agent = agent, isBackground = isBackground, endTime = parsedEndTime) | ||
| }.toList() | ||
| } | ||
|
|
||
|
|
@@ -814,7 +940,9 @@ object LyricsUtils { | |
| if (line.time > position) break // Past current position, stop early | ||
|
|
||
| // Determine this line's end time | ||
| val lineEndMs: Long = if (!line.words.isNullOrEmpty()) { | ||
| val lineEndMs: Long = if (line.endTime != null && line.endTime > line.time) { | ||
| line.endTime | ||
| } else if (!line.words.isNullOrEmpty()) { | ||
| // Use last word's endTime converted to ms | ||
| (line.words.last().endTime * 1000).toLong() | ||
| } else { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.