diff --git a/app/src/main/java/com/radiola/data/remote/CoverEnrichmentManager.kt b/app/src/main/java/com/radiola/data/remote/CoverEnrichmentManager.kt index 1d8d06c..e994483 100644 --- a/app/src/main/java/com/radiola/data/remote/CoverEnrichmentManager.kt +++ b/app/src/main/java/com/radiola/data/remote/CoverEnrichmentManager.kt @@ -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()) - private val queue = Channel(Channel.UNLIMITED) + // Что уже поставлено в общую очередь (чтобы не дублировать пачку now-playing). + private val enqueued = Collections.synchronizedSet(HashSet()) + // Что уже обработали (чтобы приоритет и общая дорожка не делали двойную работу). + private val processed = Collections.synchronizedSet(HashSet()) + + private val priority = Channel(Channel.UNLIMITED) + private val normal = Channel(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) { 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 } } diff --git a/app/src/main/java/com/radiola/data/repository/NowPlayingRepositoryImpl.kt b/app/src/main/java/com/radiola/data/repository/NowPlayingRepositoryImpl.kt index 5ce794d..3f58398 100644 --- a/app/src/main/java/com/radiola/data/repository/NowPlayingRepositoryImpl.kt +++ b/app/src/main/java/com/radiola/data/repository/NowPlayingRepositoryImpl.kt @@ -54,4 +54,6 @@ class NowPlayingRepositoryImpl @Inject constructor( Result.failure(e) } } + + override fun enrichCoverNow(track: Track) = coverEnrichment.enqueuePriority(track) } diff --git a/app/src/main/java/com/radiola/domain/repository/NowPlayingRepository.kt b/app/src/main/java/com/radiola/domain/repository/NowPlayingRepository.kt index 764b67f..e5e8173 100644 --- a/app/src/main/java/com/radiola/domain/repository/NowPlayingRepository.kt +++ b/app/src/main/java/com/radiola/domain/repository/NowPlayingRepository.kt @@ -7,4 +7,6 @@ interface NowPlayingRepository { fun getNowPlaying(stationId: Int): Flow fun getAllNowPlaying(): Flow> suspend fun refreshNowPlaying(): Result + /** Обогатить обложку трека приоритетно (тот, что слушают прямо сейчас). */ + fun enrichCoverNow(track: Track) } diff --git a/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt b/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt index 38692a3..6891f5a 100644 --- a/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt @@ -125,6 +125,10 @@ class PlayerViewModel @Inject constructor( .collect { track -> if (track != null) { _currentTrack.value = track + // Нет обложки — обогащаем приоритетно (играет прямо сейчас). + if (track.coverUrl.isNullOrBlank()) { + nowPlayingRepository.enrichCoverNow(track) + } playerController.updateMetadata( track.song, track.artist,