fix: lyrics faster + title-only search fallback for metadata mismatches
- Android: add 6s HttpTimeout to LrcLibClient, wrap each step in withTimeoutOrNull
- Both: add title-only lrclib search (step 3) so "Sanctuary (Opening)" is found
even when artist tag differs ("Utada" vs "Hikaru Utada")
- Web: reduce all timeouts 8s → 5s, extract bestMatch helper
Search order: lrclib exact → lrclib artist+title → lrclib title-only → NetEase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
71667d1a03
commit
d0df01a76d
2 files changed
+84
-51
No files matched your search
@@ -3,9 +3,11 @@ package com.novasonic.app.api
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.android.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -60,6 +62,11 @@ object LrcLibClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true; isLenient = true })
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 6_000
|
||||
connectTimeoutMillis = 5_000
|
||||
socketTimeoutMillis = 6_000
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLyrics(
|
||||
@@ -69,50 +76,65 @@ object LrcLibClient {
|
||||
durationSecs: Int?,
|
||||
): LrcLibResponse? {
|
||||
// 1. lrclib exact match
|
||||
val exact = runCatching {
|
||||
client.get("https://lrclib.net/api/get") {
|
||||
parameter("artist_name", artist)
|
||||
parameter("track_name", title)
|
||||
if (!album.isNullOrBlank()) parameter("album_name", album)
|
||||
if (durationSecs != null && durationSecs > 0) parameter("duration", durationSecs)
|
||||
}.body<LrcLibResponse>()
|
||||
}.getOrNull()
|
||||
val exact = withTimeoutOrNull(6_000) {
|
||||
runCatching {
|
||||
client.get("https://lrclib.net/api/get") {
|
||||
parameter("artist_name", artist)
|
||||
parameter("track_name", title)
|
||||
if (!album.isNullOrBlank()) parameter("album_name", album)
|
||||
if (durationSecs != null && durationSecs > 0) parameter("duration", durationSecs)
|
||||
}.body<LrcLibResponse>()
|
||||
}.getOrNull()
|
||||
}
|
||||
if (exact != null) return exact
|
||||
|
||||
// 2. lrclib fuzzy search — handles artist name mismatches
|
||||
val candidates = runCatching {
|
||||
client.get("https://lrclib.net/api/search") {
|
||||
parameter("track_name", title)
|
||||
if (artist.isNotBlank()) parameter("artist_name", artist)
|
||||
}.body<List<LrcLibResponse>>()
|
||||
}.getOrNull()
|
||||
|
||||
val lrclibResult = candidates?.let { list ->
|
||||
if (list.isEmpty()) return@let null
|
||||
if (durationSecs != null && durationSecs > 0) {
|
||||
val synced = list.filter { !it.syncedLyrics.isNullOrBlank() }
|
||||
val pool = synced.ifEmpty { list }
|
||||
pool.minByOrNull { kotlin.math.abs(it.duration - durationSecs) }
|
||||
?.takeIf { kotlin.math.abs(it.duration - durationSecs) <= 5.0 }
|
||||
?: synced.firstOrNull()
|
||||
?: list.firstOrNull()
|
||||
} else {
|
||||
list.firstOrNull { !it.syncedLyrics.isNullOrBlank() } ?: list.firstOrNull()
|
||||
}
|
||||
// 2. lrclib artist+title search — handles metadata mismatches
|
||||
val byArtist = withTimeoutOrNull(6_000) {
|
||||
runCatching {
|
||||
client.get("https://lrclib.net/api/search") {
|
||||
parameter("track_name", title)
|
||||
if (artist.isNotBlank()) parameter("artist_name", artist)
|
||||
}.body<List<LrcLibResponse>>()
|
||||
}.getOrNull()
|
||||
}
|
||||
if (lrclibResult != null) return lrclibResult
|
||||
bestMatch(byArtist, durationSecs)?.let { return it }
|
||||
|
||||
// 3. NetEase fallback — good coverage for Asian/Japanese music
|
||||
val neLrc = runCatching { netEaseLyrics(artist, title, durationSecs) }.getOrNull()
|
||||
if (!neLrc.isNullOrBlank()) {
|
||||
return LrcLibResponse(syncedLyrics = neLrc)
|
||||
// 3. lrclib title-only search — catches artist name mismatches like "Utada" vs "Hikaru Utada"
|
||||
val byTitle = withTimeoutOrNull(6_000) {
|
||||
runCatching {
|
||||
client.get("https://lrclib.net/api/search") {
|
||||
parameter("track_name", title)
|
||||
}.body<List<LrcLibResponse>>()
|
||||
}.getOrNull()
|
||||
}
|
||||
bestMatch(byTitle, durationSecs)?.let { return it }
|
||||
|
||||
// 4. NetEase fallback — good coverage for Asian/Japanese music
|
||||
val neLrc = withTimeoutOrNull(8_000) {
|
||||
runCatching { netEaseLyrics(artist, title, durationSecs) }.getOrNull()
|
||||
}
|
||||
if (!neLrc.isNullOrBlank()) return LrcLibResponse(syncedLyrics = neLrc)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun bestMatch(candidates: List<LrcLibResponse>?, durationSecs: Int?): LrcLibResponse? {
|
||||
if (candidates.isNullOrEmpty()) return null
|
||||
return if (durationSecs != null && durationSecs > 0) {
|
||||
val synced = candidates.filter { !it.syncedLyrics.isNullOrBlank() }
|
||||
val pool = synced.ifEmpty { candidates }
|
||||
pool.minByOrNull { kotlin.math.abs(it.duration - durationSecs) }
|
||||
?.takeIf { kotlin.math.abs(it.duration - durationSecs) <= 5.0 }
|
||||
?: synced.firstOrNull()
|
||||
?: candidates.firstOrNull()
|
||||
} else {
|
||||
candidates.firstOrNull { !it.syncedLyrics.isNullOrBlank() }
|
||||
?: candidates.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun netEaseLyrics(artist: String, title: String, durationSecs: Int?): String? {
|
||||
val query = "$artist $title".trim()
|
||||
val query = "$artist $title".trim()
|
||||
val search = client.get("https://music.163.com/api/search/get") {
|
||||
parameter("s", query)
|
||||
parameter("type", 1)
|
||||
@@ -121,7 +143,7 @@ object LrcLibClient {
|
||||
}.body<NetEaseSearchResp>()
|
||||
|
||||
val songs = search.result?.songs ?: return null
|
||||
val song = if (durationSecs != null && durationSecs > 0) {
|
||||
val song = if (durationSecs != null && durationSecs > 0) {
|
||||
val durMs = durationSecs * 1000L
|
||||
songs.minByOrNull { kotlin.math.abs(it.duration - durMs) }
|
||||
?.takeIf { kotlin.math.abs(it.duration - durMs) <= 5000L }
|
||||
|
||||
Reference in new issue
Block a user