Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package com.metrolist.music.lyrics

import kotlinx.coroutines.flow.MutableStateFlow
import com.metrolist.music.ui.screens.settings.LyricsPosition

data class WordTimestamp(
val text: String,
Expand All @@ -21,7 +22,9 @@ data class LyricsEntry(
val romanizedTextFlow: MutableStateFlow<String?> = MutableStateFlow(null),
val translatedTextFlow: MutableStateFlow<String?> = MutableStateFlow(null),
val agent: String? = null,
val isBackground: Boolean = false
val isBackground: Boolean = false,
val endTime: Long? = null,
var linePosition: LyricsPosition? = null
) : Comparable<LyricsEntry> {
override fun compareTo(other: LyricsEntry): Int = (time - other.time).toInt()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.getOrNull()
?.takeIf { it.isNotBlank() }
?: return null
Expand All @@ -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
Expand All @@ -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")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

var lastWasBg = false

for (line in lyrics) {
Expand All @@ -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)
}

Expand All @@ -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)
}
Expand All @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the LyricsPlusProvider.kt file
find . -name "LyricsPlusProvider.kt" -type f

Repository: 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 -l

Repository: 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.kt

Repository: 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.kt

Repository: 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.kt

Repository: MetrolistGroup/Metrolist

Length of output: 273


🏁 Script executed:

# Find TTMLParser.kt to verify the claim about omitting trailing time
find . -name "TTMLParser.kt" -type f

Repository: 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.kt

Repository: 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.kt

Repository: 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.kt

Repository: 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.kt

Repository: 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 -5

Repository: 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 -10

Repository: 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 3

Repository: 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.kt

Repository: 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 2

Repository: 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 2

Repository: 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-list

Repository: 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.kt

Repository: 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 50

Repository: 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 -120

Repository: 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 2

Repository: 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 -80

Repository: 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 80

Repository: 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 60

Repository: 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.kt

Repository: MetrolistGroup/Metrolist

Length of output: 856


Update appendLrcLine to omit trailing timestamp for word-sync lines and fix stale KDoc.

appendLrcLine unconditionally appends [end] whenever durationMs > 0, but TTMLParser.toLRC deliberately omits the trailing timestamp when word-sync data is present. While LyricsUtils.parseLine tolerates and strips trailing timestamps with word blocks, this creates an inconsistency and unnecessary output. For consistency, skip the trailing timestamp when isWordSync is true. The KDoc on line 372 is also stale and should be updated to reflect the conditional trailing-timestamp behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/lyrics/LyricsPlusProvider.kt` around
lines 372 - 381, The appendLrcLine function currently always appends a trailing
timestamp when durationMs > 0; update it to accept an additional Boolean
parameter (e.g., isWordSync) and skip appending the end timestamp
(formatLrcTime(timeMs + durationMs)) when isWordSync is true to match
TTMLParser.toLRC behavior and avoid redundant timestamps that
LyricsUtils.parseLine already tolerates; also update the KDoc to describe that
the trailing timestamp is conditional based on isWordSync. Ensure callers
(notably TTMLParser.toLRC) are updated to pass the correct isWordSync flag when
invoking appendLrcLine.


Expand Down
154 changes: 141 additions & 13 deletions app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
}
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Only strip full agent-header lines.

Using find() drops any lyric line that merely contains [agent:...] in its text. Treat it as metadata only when the trimmed line is exactly the header.

Proposed fix
-            val agentMatch = AGENT_HEADER_REGEX.find(trimmed)
+            val agentMatch = AGENT_HEADER_REGEX.matchEntire(trimmed)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt` around lines
452 - 460, The filter over rawLines currently uses
AGENT_HEADER_REGEX.find(trimmed) which matches any line containing an agent
header anywhere and incorrectly drops lyric lines that include “[agent:...]”
inline; change the condition in the rawLines.filter within the code that builds
lines/agents (the block that references AGENT_HEADER_REGEX, agents,
LyricsAgentMetadata and alias/type/name) to only treat the line as metadata when
the entire trimmed line exactly matches the agent header (e.g., use a full-match
check rather than find), so only whole-agent-header lines are removed and inline
occurrences remain in the returned lines.

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

/**
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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()
Expand All @@ -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()
}

Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading