feat(app): кнопка «Распознать трек» (Shazam) + история распознанных
- кнопка распознавания в плеере: видна только на музыкальных станциях без
метаданных эфира (track == null), показывает спиннер и результат через Toast
- распознанный трек отображается в плеере и пишется в ОТДЕЛЬНУЮ историю
распознанных (не дублируется в историю эфирных треков — гейт по ключу)
- экран Истории: переключатель «Треки эфира | Распознанные», два списка
- Room: таблица recognized_track (миграция 7→8), DAO/репозиторий
- ShazamRepository → POST /shazam/recognize/{stationId}, маппинг 503/400 в текст
- MusicGenres.isMusicStation — клиентский гейт (синхронизирован с бэкендом)
- bump backend submodule (модуль shazam)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ import com.radiola.domain.repository.RecordingRepository
|
||||
import com.radiola.domain.usecase.GetNowPlayingUseCase
|
||||
import com.radiola.domain.usecase.GetStationsUseCase
|
||||
import com.radiola.domain.usecase.SearchTrackInServiceUseCase
|
||||
import com.radiola.domain.repository.RecognizeResult
|
||||
import com.radiola.domain.repository.RecognizedTrackRepository
|
||||
import com.radiola.domain.repository.ShazamRepository
|
||||
import com.radiola.domain.repository.TrackHistoryRepository
|
||||
import com.radiola.domain.usecase.ToggleFavoriteUseCase
|
||||
import com.radiola.domain.usecase.auth.PushHistoryUseCase
|
||||
@@ -34,6 +37,8 @@ class PlayerViewModel @Inject constructor(
|
||||
private val searchTrackInServiceUseCase: SearchTrackInServiceUseCase,
|
||||
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
|
||||
private val trackHistoryRepository: TrackHistoryRepository,
|
||||
private val recognizedTrackRepository: RecognizedTrackRepository,
|
||||
private val shazamRepository: ShazamRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val recordingRepository: RecordingRepository,
|
||||
private val pushHistoryUseCase: PushHistoryUseCase,
|
||||
@@ -54,6 +59,17 @@ class PlayerViewModel @Inject constructor(
|
||||
private val _currentTrack = MutableStateFlow<Track?>(null)
|
||||
val currentTrack: StateFlow<Track?> = _currentTrack.asStateFlow()
|
||||
|
||||
// Распознавание трека (Shazam) — индикатор и одноразовые сообщения для UI.
|
||||
private val _recognizing = MutableStateFlow(false)
|
||||
val recognizing: StateFlow<Boolean> = _recognizing.asStateFlow()
|
||||
|
||||
private val _recognizeEvent = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||
val recognizeEvent: SharedFlow<String> = _recognizeEvent.asSharedFlow()
|
||||
|
||||
// Ключ трека, добавленного через распознавание — его НЕ дублируем в историю
|
||||
// «эфирных» треков (он идёт в отдельную историю распознанных).
|
||||
private var recognizedKey: String? = null
|
||||
|
||||
private val _enabledServices = MutableStateFlow<List<DeeplinkService>>(emptyList())
|
||||
val enabledServices: StateFlow<List<DeeplinkService>> = _enabledServices.asStateFlow()
|
||||
|
||||
@@ -103,6 +119,8 @@ class PlayerViewModel @Inject constructor(
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.collect { track ->
|
||||
// Распознанный трек уже в истории распознанных — не дублируем в эфирную.
|
||||
if (trackKey(track) == recognizedKey) return@collect
|
||||
trackHistoryRepository.addTrack(track)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +132,7 @@ class PlayerViewModel @Inject constructor(
|
||||
recordingPlaybackController.stop()
|
||||
_currentStation.value = station
|
||||
_currentTrack.value = null
|
||||
recognizedKey = null
|
||||
_playlist.value = playlist ?: _stations.value
|
||||
// Выбираем стартовое качество: предпочтение пользователя → совпадение с
|
||||
// потоком по умолчанию → высшее. Если вариантов нет — играем как есть.
|
||||
@@ -264,6 +283,32 @@ class PlayerViewModel @Inject constructor(
|
||||
return searchTrackInServiceUseCase(track, service)
|
||||
}
|
||||
|
||||
/** Распознать играющий сейчас трек через Shazam (бэкенд тянет аудио из потока). */
|
||||
fun recognizeCurrentTrack() {
|
||||
val station = _currentStation.value ?: return
|
||||
if (_recognizing.value) return
|
||||
_recognizing.value = true
|
||||
viewModelScope.launch {
|
||||
when (val r = shazamRepository.recognize(station.id, station.name)) {
|
||||
is RecognizeResult.Found -> {
|
||||
recognizedKey = trackKey(r.track)
|
||||
_currentTrack.value = r.track
|
||||
recognizedTrackRepository.addTrack(r.track)
|
||||
playerController.updateMetadata(
|
||||
r.track.song, r.track.artist, r.track.coverUrl ?: "", station.name
|
||||
)
|
||||
_recognizeEvent.emit("Распознано: ${r.track.artist} — ${r.track.song}")
|
||||
}
|
||||
is RecognizeResult.NotFound -> _recognizeEvent.emit("Не удалось распознать трек")
|
||||
is RecognizeResult.Error -> _recognizeEvent.emit(r.message)
|
||||
}
|
||||
_recognizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun trackKey(t: Track): String =
|
||||
(t.artist.trim() + "|" + t.song.trim()).lowercase()
|
||||
|
||||
fun toggleFavorite(station: Station) {
|
||||
viewModelScope.launch {
|
||||
toggleFavoriteUseCase(station)
|
||||
|
||||
Reference in New Issue
Block a user