package com.radiola.ui.player import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.radiola.domain.model.DeeplinkService import com.radiola.domain.model.Station import com.radiola.domain.model.StreamQuality import com.radiola.domain.model.Track import com.radiola.domain.repository.SettingsRepository import com.radiola.domain.repository.StationRepository import com.radiola.domain.repository.NowPlayingRepository 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.TrackHistoryRepository import com.radiola.domain.usecase.ToggleFavoriteUseCase import com.radiola.domain.usecase.auth.PushHistoryUseCase import com.radiola.service.PlayerController import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class PlayerViewModel @Inject constructor( private val playerController: PlayerController, private val stationRepository: StationRepository, private val nowPlayingRepository: NowPlayingRepository, private val getStationsUseCase: GetStationsUseCase, private val getNowPlayingUseCase: GetNowPlayingUseCase, private val searchTrackInServiceUseCase: SearchTrackInServiceUseCase, private val toggleFavoriteUseCase: ToggleFavoriteUseCase, private val trackHistoryRepository: TrackHistoryRepository, private val settingsRepository: SettingsRepository, private val recordingRepository: RecordingRepository, private val pushHistoryUseCase: PushHistoryUseCase, private val loveStreamResolver: com.radiola.data.remote.LoveStreamResolver ) : ViewModel() { val isPlaying: StateFlow = playerController.isPlaying val currentStationPrefix: StateFlow = playerController.currentStationPrefix private val _currentStation = MutableStateFlow(null) val currentStation: StateFlow = _currentStation.asStateFlow() private val _currentTrack = MutableStateFlow(null) val currentTrack: StateFlow = _currentTrack.asStateFlow() private val _enabledServices = MutableStateFlow>(emptyList()) val enabledServices: StateFlow> = _enabledServices.asStateFlow() private val _stations = MutableStateFlow>(emptyList()) val stations: StateFlow> = _stations.asStateFlow() private val _playlist = MutableStateFlow>(emptyList()) val playlist: StateFlow> = _playlist.asStateFlow() // Выбранное качество текущей станции (битрейт). null — у станции нет вариантов. private val _currentQuality = MutableStateFlow(null) val currentQuality: StateFlow = _currentQuality.asStateFlow() // Предпочитаемый битрейт пользователя (0 = авто/по умолчанию станции). private var preferredBitrate: Int = 0 val isRecording: StateFlow = recordingRepository.isRecording private var nowPlayingJob: Job? = null init { playerController.onSkipToNext = { playNext() } playerController.onSkipToPrevious = { playPrevious() } viewModelScope.launch { getStationsUseCase().collect { _stations.value = it } } viewModelScope.launch { settingsRepository.getEnabledDeeplinkServices().collect { ids -> _enabledServices.value = DeeplinkService.entries.filter { it.serviceId in ids } } } viewModelScope.launch { settingsRepository.getPreferredBitrate().collect { preferredBitrate = it } } viewModelScope.launch { _currentTrack .filterNotNull() .distinctUntilChanged() .collect { track -> trackHistoryRepository.addTrack(track) } } } fun play(station: Station, playlist: List? = null) { _currentStation.value = station _currentTrack.value = null _playlist.value = playlist ?: _stations.value // Выбираем стартовое качество: предпочтение пользователя → совпадение с // потоком по умолчанию → высшее. Если вариантов нет — играем как есть. val quality = pickInitialQuality(station) _currentQuality.value = quality val streamUrl = quality?.url ?: station.streamUrl // Love Radio: подставляем сессионный UID (иначе поток отдаёт заглушку). // Для остальных resolve вернёт URL как есть. viewModelScope.launch { val url = loveStreamResolver.resolve(streamUrl) playerController.play(url, station.prefix, station.name, station.id) } viewModelScope.launch { pushHistoryUseCase(station.id) } nowPlayingJob?.cancel() nowPlayingJob = viewModelScope.launch { // Polling loop for Record API now playing launch { while (true) { nowPlayingRepository.refreshNowPlaying() delay(10_000) } } // Collect now playing for this station (API has priority: covers + accurate metadata) launch { getNowPlayingUseCase(station.id) .distinctUntilChanged() .collect { track -> if (track != null) { _currentTrack.value = track // Нет обложки — обогащаем приоритетно (играет прямо сейчас). if (track.coverUrl.isNullOrBlank()) { nowPlayingRepository.enrichCoverNow(track) } playerController.updateMetadata( track.song, track.artist, track.coverUrl ?: "", station.name ) } } } // Fallback: Icy metadata from stream for stations not in Record API launch { playerController.icyTitle .filterNotNull() .distinctUntilChanged() .collect { icyTitle -> // Only use Icy if no API track is currently active if (_currentTrack.value == null) { val track = parseIcyTitle(icyTitle) if (track != null) { _currentTrack.value = track playerController.updateMetadata( track.song, track.artist, "", station.name ) } } } } } } /** Стартовое качество станции с учётом предпочтения пользователя. */ private fun pickInitialQuality(station: Station): StreamQuality? { val list = station.qualities if (list.size < 2) return null return list.firstOrNull { it.bitrate == preferredBitrate } ?: list.firstOrNull { it.url == station.streamUrl } ?: list.first() } /** Переключить качество текущей станции на лету (без сброса now-playing). */ fun selectQuality(quality: StreamQuality) { val station = _currentStation.value ?: return if (_currentQuality.value?.bitrate == quality.bitrate) return _currentQuality.value = quality preferredBitrate = quality.bitrate viewModelScope.launch { settingsRepository.setPreferredBitrate(quality.bitrate) } viewModelScope.launch { val url = loveStreamResolver.resolve(quality.url) playerController.changeStream(url) } } private fun parseIcyTitle(title: String?): Track? { if (title.isNullOrBlank()) return null val separators = listOf(" - ", " — ", " – ") for (sep in separators) { val parts = title.split(sep, limit = 2) if (parts.size == 2) { return Track( artist = parts[0].trim(), song = parts[1].trim(), coverUrl = null, stationName = _currentStation.value?.name ?: "" ) } } // No separator found: treat entire string as song title return Track( artist = "", song = title.trim(), coverUrl = null, stationName = _currentStation.value?.name ?: "" ) } fun pause() { playerController.pause() } fun resume() { playerController.play() } fun togglePlayPause() { if (isPlaying.value) pause() else resume() } fun playNext() { val current = _currentStation.value ?: return val list = _playlist.value if (list.isEmpty()) return val index = list.indexOfFirst { it.id == current.id } val next = list.getOrNull((index + 1).mod(list.size)) next?.let { play(it, list) } } fun playPrevious() { val current = _currentStation.value ?: return val list = _playlist.value if (list.isEmpty()) return val index = list.indexOfFirst { it.id == current.id } val prev = list.getOrNull((index - 1).mod(list.size)) prev?.let { play(it, list) } } fun getDeeplinkUrl(track: Track, service: DeeplinkService): String { return searchTrackInServiceUseCase(track, service) } fun toggleFavorite(station: Station) { viewModelScope.launch { toggleFavoriteUseCase(station) } } fun toggleRecording() { viewModelScope.launch { if (recordingRepository.isRecording.value) { recordingRepository.stopRecording() } else { val station = _currentStation.value ?: return@launch recordingRepository.startRecording(station, _currentTrack.value) } } } }