fix: lrclib rate limit — exact first, then 2 parallel searches; check HTTP 200
3 simultaneous requests were hitting lrclib rate limiter. Now: exact match alone → if no content, 2 parallel searches. Also check response.status === 200 before parsing to avoid treating 404/429 error bodies as valid empty lyrics responses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7f8b301ab
commit
eb5b0ea360
2 files changed
+42
-34
No files matched your search
@@ -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<String, LrcLibResponse?>()
|
||||
|
||||
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") {
|
||||
// 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)
|
||||
}.body<LrcLibResponse>()
|
||||
}.getOrNull()
|
||||
}
|
||||
if (resp.status == HttpStatusCode.OK) resp.body<LrcLibResponse>() 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<List<LrcLibResponse>>()
|
||||
}
|
||||
if (resp.status == HttpStatusCode.OK) resp.body<List<LrcLibResponse>>() 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<List<LrcLibResponse>>()
|
||||
}
|
||||
if (resp.status == HttpStatusCode.OK) resp.body<List<LrcLibResponse>>() 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
|
||||
}
|
||||
|
||||
// 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<LrcLibResponse>?, durationSecs: Int?): LrcLibResponse? {
|
||||
if (candidates.isNullOrEmpty()) return null
|
||||
val synced = candidates.filter { !it.syncedLyrics.isNullOrBlank() }
|
||||
|
||||
@@ -174,26 +174,36 @@ async function _loadNpLyrics(songId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire all three lrclib requests in parallel — total wait = slowest single request
|
||||
// 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);
|
||||
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;
|
||||
|
||||
// 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 [exactData, artistData, titleData] = await Promise.all([
|
||||
safeJson(fetch(`https://lrclib.net/api/get?${exactP}`, { signal: SIG() })),
|
||||
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;
|
||||
|
||||
let result = (exactData?.syncedLyrics || exactData?.plainLyrics ? exactData : null)
|
||||
?? bestMatch(artistData, dur)
|
||||
?? bestMatch(titleData, dur);
|
||||
|
||||
// NetEase fallback — good for Asian/Japanese music
|
||||
if(!result?.syncedLyrics && !result?.plainLyrics) {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user