fix(lyrics): stop hammering lrclib (caused IP throttling → no lyrics)
Rapid skips + lyrics prefetch fired the full lrclib cascade many times over, so lrclib throttled the IP and requests timed out (canceled) on both web and Android. Reduce the request volume: - Web: de-dupe concurrent fetches per song (prefetch + tab-open now share one cascade); bump the per-request timeout 6s → 8s. - Android: per-song Mutex so prefetch + on-open load don't both run the cascade; send a proper User-Agent (lrclib asks for one and treats it better); debounce the prefetch (1.5s, cancelled on the next skip) instead of firing immediately on every track change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
ddb9418603
commit
63a7260c94
3 files changed
+34
-3
No files matched your search
@@ -10,6 +10,8 @@ import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -55,9 +57,13 @@ object LrcLibClient {
|
||||
connectTimeoutMillis = 5_000
|
||||
socketTimeoutMillis = 8_000
|
||||
}
|
||||
// lrclib asks clients to identify themselves; a proper UA also avoids being throttled.
|
||||
install(UserAgent) { agent = "NovaSonic/1.0 (+https://gitea.honeyy.duckdns.org/Docker/NovaSonic)" }
|
||||
}
|
||||
|
||||
private val cache = HashMap<String, LrcLibResponse?>()
|
||||
// Per-song lock so a prefetch + an on-open load don't both fire the whole cascade (→ throttling).
|
||||
private val locks = java.util.concurrent.ConcurrentHashMap<String, Mutex>()
|
||||
|
||||
suspend fun getLyrics(
|
||||
artist: String,
|
||||
@@ -67,6 +73,18 @@ object LrcLibClient {
|
||||
): LrcLibResponse? {
|
||||
val cacheKey = "$artist|$title"
|
||||
if (cache.containsKey(cacheKey)) return cache[cacheKey]
|
||||
val mutex = locks.getOrPut(cacheKey) { Mutex() }
|
||||
return mutex.withLock { getLyricsInner(artist, title, album, durationSecs, cacheKey) }
|
||||
}
|
||||
|
||||
private suspend fun getLyricsInner(
|
||||
artist: String,
|
||||
title: String,
|
||||
album: String?,
|
||||
durationSecs: Int?,
|
||||
cacheKey: String,
|
||||
): LrcLibResponse? {
|
||||
if (cache.containsKey(cacheKey)) return cache[cacheKey] // populated while we waited on the lock
|
||||
|
||||
// 1a. lrclib exact match with duration
|
||||
val exact = runCatching {
|
||||
|
||||
@@ -280,9 +280,13 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
|
||||
)
|
||||
}
|
||||
|
||||
/** Warm the LrcLib cache as soon as a track starts, so opening Lyrics feels instant. */
|
||||
private var _lyricsPrefetchJob: Job? = null
|
||||
/** Warm the LrcLib cache shortly after a track settles. Debounced so rapid skips don't
|
||||
* hammer lrclib (which throttles the IP and then no lyrics load at all). */
|
||||
private fun prefetchLyrics(song: Song) {
|
||||
scope.launch {
|
||||
_lyricsPrefetchJob?.cancel()
|
||||
_lyricsPrefetchJob = scope.launch {
|
||||
delay(1500) // skip quickly without firing a request per track
|
||||
runCatching {
|
||||
LrcLibClient.getLyrics(
|
||||
artist = song.artist ?: "",
|
||||
|
||||
Reference in new issue
Block a user