diff --git a/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt b/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt index d7fca68..79f7b4c 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt @@ -6,6 +6,7 @@ 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.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -61,13 +62,12 @@ object LrcLibClient { json(Json { ignoreUnknownKeys = true; isLenient = true }) } install(HttpTimeout) { - requestTimeoutMillis = 7_000 + requestTimeoutMillis = 8_000 connectTimeoutMillis = 5_000 - socketTimeoutMillis = 7_000 + socketTimeoutMillis = 8_000 } } - // Simple in-memory cache — avoids re-fetching when reopening lyrics for the same song private val cache = HashMap() suspend fun getLyrics( @@ -79,50 +79,45 @@ object LrcLibClient { val cacheKey = "$artist|$title" if (cache.containsKey(cacheKey)) return cache[cacheKey] - // Fire all three lrclib requests in parallel — total wait = slowest single request - val lrclibResult = coroutineScope { - val exactDef = async { - 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() - }.getOrNull() + // 1. lrclib exact match — single request, only trust HTTP 200 + val exact = runCatching { + val resp = 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) } + if (resp.status == HttpStatusCode.OK) resp.body() else null + }.getOrNull() + + if (exact.hasContent()) return exact.also { cache[cacheKey] = it } + + // 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3) + val lrclibResult = coroutineScope { val artistDef = async { runCatching { - client.get("https://lrclib.net/api/search") { + val resp = client.get("https://lrclib.net/api/search") { parameter("track_name", title) if (artist.isNotBlank()) parameter("artist_name", artist) - }.body>() + } + if (resp.status == HttpStatusCode.OK) resp.body>() else null }.getOrNull() } val titleDef = async { runCatching { - client.get("https://lrclib.net/api/search") { + val resp = client.get("https://lrclib.net/api/search") { parameter("track_name", title) - }.body>() + } + if (resp.status == HttpStatusCode.OK) resp.body>() else null }.getOrNull() } - - val exact = exactDef.await() - if (exact != null) { - artistDef.cancel() - titleDef.cancel() - exact - } else { - bestMatch(artistDef.await(), durationSecs) - ?: bestMatch(titleDef.await(), durationSecs) - } - } - if (lrclibResult != null) { - cache[cacheKey] = lrclibResult - return lrclibResult + bestMatch(artistDef.await(), durationSecs) + ?: bestMatch(titleDef.await(), durationSecs) } - // NetEase fallback — good for Asian/Japanese music + if (lrclibResult != null) return lrclibResult.also { cache[cacheKey] = it } + + // 4. NetEase fallback val neLrc = withTimeoutOrNull(8_000) { runCatching { netEaseLyrics(artist, title, durationSecs) }.getOrNull() } @@ -131,6 +126,9 @@ object LrcLibClient { return result } + private fun LrcLibResponse?.hasContent() = + this != null && (instrumental || !syncedLyrics.isNullOrBlank() || !plainLyrics.isNullOrBlank()) + private fun bestMatch(candidates: List?, durationSecs: Int?): LrcLibResponse? { if (candidates.isNullOrEmpty()) return null val synced = candidates.filter { !it.syncedLyrics.isNullOrBlank() } diff --git a/src/player/nowplaying.js b/src/player/nowplaying.js index 2358ea0..0ff1361 100644 --- a/src/player/nowplaying.js +++ b/src/player/nowplaying.js @@ -174,25 +174,35 @@ async function _loadNpLyrics(songId) { return; } - // Fire all three lrclib requests in parallel — total wait = slowest single request - const exactP = new URLSearchParams({ artist_name: artist, track_name: title }); + // 1. lrclib exact match — single request, only trust HTTP 200 + const exactP = new URLSearchParams({ artist_name: artist, track_name: title }); if(album) exactP.set('album_name', album); if(dur) exactP.set('duration', dur); - const artistP = new URLSearchParams({ track_name: title }); - if(artist) artistP.set('artist_name', artist); - const titleP = new URLSearchParams({ track_name: title }); - - const [exactData, artistData, titleData] = await Promise.all([ - safeJson(fetch(`https://lrclib.net/api/get?${exactP}`, { signal: SIG() })), - safeJson(fetch(`https://lrclib.net/api/search?${artistP}`, { signal: SIG() })), - safeJson(fetch(`https://lrclib.net/api/search?${titleP}`, { signal: SIG() })), - ]); + let result = null; + try { + const r = await fetch(`https://lrclib.net/api/get?${exactP}`, { signal: SIG() }); + if(r.status === 200) { + const d = await r.json(); + if(d?.syncedLyrics || d?.plainLyrics || d?.instrumental) result = d; + } + } catch {} if(token !== _lyricsLoadToken) return; - let result = (exactData?.syncedLyrics || exactData?.plainLyrics ? exactData : null) - ?? bestMatch(artistData, dur) - ?? bestMatch(titleData, dur); + // 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3) + if(!result) { + const artistP = new URLSearchParams({ track_name: title }); + if(artist) artistP.set('artist_name', artist); + const titleP = new URLSearchParams({ track_name: title }); + + const [artistData, titleData] = await Promise.all([ + safeJson(fetch(`https://lrclib.net/api/search?${artistP}`, { signal: SIG() })), + safeJson(fetch(`https://lrclib.net/api/search?${titleP}`, { signal: SIG() })), + ]); + result = bestMatch(artistData, dur) ?? bestMatch(titleData, dur); + } + + if(token !== _lyricsLoadToken) return; // NetEase fallback — good for Asian/Japanese music if(!result?.syncedLyrics && !result?.plainLyrics) {