feat(covers): приоритет играющего трека + троттл 0.8с
Обложки наливались общей очередью (1.5с) — играющий трек ждал свою очередь. Добавлена приоритетная дорожка: трек, который слушают сейчас, обогащается первым (PlayerViewModel → NowPlayingRepository.enrichCoverNow). Троттл общей очереди ускорен 1.5с→0.8с. Дедуп разнесён на enqueued/processed, чтобы дорожки не дублировали работу. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -16,8 +17,11 @@ import javax.inject.Singleton
|
||||
* Клиентское обогащение обложек. Серверный IP забанен Apple (429), поэтому
|
||||
* iTunes-поиск делаем С УСТРОЙСТВА пользователя (его IP не забанен), а найденную
|
||||
* ссылку на арт шлём на наш бэкенд — он скачивает её и кладёт WebP к себе.
|
||||
* Дальше обложка приходит ВСЕМ через /now-playing. Дедуп по треку (в рамках
|
||||
* сессии) + троттлинг, чтобы не злоупотреблять iTunes с устройства.
|
||||
* Дальше обложка приходит ВСЕМ через /now-playing.
|
||||
*
|
||||
* Две дорожки: приоритетная (трек, который слушают прямо сейчас — обрабатывается
|
||||
* первой) и общая (остальные now-playing). Дедуп + троттлинг, чтобы не
|
||||
* злоупотреблять iTunes с устройства.
|
||||
*/
|
||||
@Singleton
|
||||
class CoverEnrichmentManager @Inject constructor(
|
||||
@@ -25,28 +29,47 @@ class CoverEnrichmentManager @Inject constructor(
|
||||
private val radiolaApi: RadiolaApi,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val seen = Collections.synchronizedSet(HashSet<String>())
|
||||
private val queue = Channel<Track>(Channel.UNLIMITED)
|
||||
// Что уже поставлено в общую очередь (чтобы не дублировать пачку now-playing).
|
||||
private val enqueued = Collections.synchronizedSet(HashSet<String>())
|
||||
// Что уже обработали (чтобы приоритет и общая дорожка не делали двойную работу).
|
||||
private val processed = Collections.synchronizedSet(HashSet<String>())
|
||||
|
||||
private val priority = Channel<Track>(Channel.UNLIMITED)
|
||||
private val normal = Channel<Track>(Channel.UNLIMITED)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
for (track in queue) {
|
||||
while (true) {
|
||||
val track = priority.tryReceive().getOrNull() ?: select {
|
||||
priority.onReceive { it }
|
||||
normal.onReceive { it }
|
||||
}
|
||||
processOne(track)
|
||||
delay(THROTTLE_MS) // не долбить iTunes с устройства
|
||||
delay(THROTTLE_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Поставить now-playing-треки без обложки в очередь обогащения. */
|
||||
/** Поставить пачку now-playing-треков без обложки в общую очередь. */
|
||||
fun enqueue(tracks: Collection<Track>) {
|
||||
for (t in tracks) {
|
||||
if (!t.coverUrl.isNullOrBlank()) continue
|
||||
if (t.artist.isBlank() || t.song.isBlank()) continue
|
||||
if (seen.add(normKey(t))) queue.trySend(t)
|
||||
if (!isEnrichable(t)) continue
|
||||
if (enqueued.add(normKey(t))) normal.trySend(t)
|
||||
}
|
||||
}
|
||||
|
||||
/** Трек, который слушают прямо сейчас — вперёд очереди (вызывать при смене трека). */
|
||||
fun enqueuePriority(track: Track?) {
|
||||
if (track == null || !isEnrichable(track)) return
|
||||
priority.trySend(track)
|
||||
}
|
||||
|
||||
private fun isEnrichable(t: Track): Boolean =
|
||||
t.coverUrl.isNullOrBlank() && t.artist.isNotBlank() && t.song.isNotBlank()
|
||||
|
||||
private suspend fun processOne(track: Track) {
|
||||
val key = normKey(track)
|
||||
if (!processed.add(key)) return // уже обрабатывали (другая дорожка)
|
||||
try {
|
||||
val term = clean("${track.artist} ${track.song}")
|
||||
if (term.isBlank()) return
|
||||
@@ -56,7 +79,8 @@ class CoverEnrichmentManager @Inject constructor(
|
||||
val resp = radiolaApi.submitCover(SubmitCoverDto(track.artist, track.song, big))
|
||||
android.util.Log.d("CoverEnrich", "submit '${track.artist} - ${track.song}' -> ${resp.coverUrl}")
|
||||
} catch (_: Exception) {
|
||||
// сеть/429/таймаут — не критично, добёрут другие клиенты или попытки
|
||||
// сеть/429/таймаут — не критично; снимаем метку, чтобы могли попробовать позже
|
||||
processed.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +95,6 @@ class CoverEnrichmentManager @Inject constructor(
|
||||
.trim()
|
||||
|
||||
companion object {
|
||||
private const val THROTTLE_MS = 1500L
|
||||
private const val THROTTLE_MS = 800L
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user