package com.lofiradio.ui import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lofiradio.data.CHANNELS import com.lofiradio.data.models.Channel import com.lofiradio.data.models.StreamInfo import com.lofiradio.network.ServerApi import com.lofiradio.network.YouTubeExtractor import com.lofiradio.player.AudioPlayer import com.lofiradio.util.Preferences import kotlinx.coroutines.* class LofiViewModel( private val context: Context, private val serverApi: ServerApi, private val audioPlayer: AudioPlayer ) : ViewModel() { private val _channelList = mutableListOf() private val _channels = androidx.lifecycle.MutableLiveData>(emptyList()) val channels: androidx.lifecycle.LiveData> get() = _channels private fun notifyChannelsChanged() { _channels.value = _channelList.toList() } private val _currentChannel = androidx.lifecycle.MutableLiveData(null) val currentChannel: androidx.lifecycle.LiveData get() = _currentChannel private val _playerState = androidx.lifecycle.MutableLiveData(PlayerState.IDLE) val playerState: androidx.lifecycle.LiveData get() = _playerState private val _isLoading = androidx.lifecycle.MutableLiveData(false) val isLoading: androidx.lifecycle.LiveData get() = _isLoading private val _error = androidx.lifecycle.MutableLiveData(null) val error: androidx.lifecycle.LiveData get() = _error private val _isDiscovering = androidx.lifecycle.MutableLiveData(false) val isDiscovering: androidx.lifecycle.LiveData get() = _isDiscovering private var currentTab: Tab = Tab.ALL enum class Tab { ALL, FAVORITES, LIVE } enum class PlayerState { IDLE, PLAYING, PAUSED, BUFFERING, ERROR, ENDED } init { _channelList.addAll(CHANNELS.map { it.copy() }) notifyChannelsChanged() audioPlayer.onStateChanged = { state -> when (state) { androidx.media3.common.Player.STATE_READY -> { if (audioPlayer.isPlaying) { _playerState.postValue(PlayerState.PLAYING) } else { _playerState.postValue(PlayerState.PAUSED) } } androidx.media3.common.Player.STATE_BUFFERING -> { _playerState.postValue(PlayerState.BUFFERING) } androidx.media3.common.Player.STATE_ENDED -> { _playerState.postValue(PlayerState.ENDED) if (Preferences.isAutoAdvance(context)) { playNext() } } androidx.media3.common.Player.STATE_IDLE -> { _playerState.postValue(PlayerState.IDLE) } } false } audioPlayer.onError = { message -> _error.postValue(message) _playerState.postValue(PlayerState.ERROR) } } fun getVisibleChannels(): List { return when (currentTab) { Tab.ALL -> _channelList Tab.FAVORITES -> _channelList.filter { Preferences.isFavorite(context, it.id) } Tab.LIVE -> _channelList.filter { it.isLive } } } fun setTab(tab: Tab) { currentTab = tab } fun toggleFavorite(channelId: String) { Preferences.toggleFavorite(context, channelId) val idx = _channelList.indexOfFirst { it.id == channelId } if (idx != -1) { val fav = Preferences.isFavorite(context, channelId) _channelList[idx] = _channelList[idx].copy(isFavorite = fav) notifyChannelsChanged() } } fun playChannel(channel: Channel) { _error.value = null if (channel.videoId.isNullOrBlank()) { _error.value = "Channel is not currently live" _playerState.value = PlayerState.ERROR return } when (Preferences.getStreamingMode(context)) { Preferences.StreamingMode.SERVER -> { playViaServer(channel) } Preferences.StreamingMode.DIRECT -> { playViaDirect(channel) } } } private fun playViaServer(channel: Channel) { _isLoading.value = true viewModelScope.launch { try { val stream = serverApi.getStream(channel.videoId!!) if (stream != null) { audioPlayer.play(stream.videoId, stream.url, stream.streamType) _currentChannel.value = channel _playerState.value = PlayerState.PLAYING } else { _error.value = "Could not get stream from server" _playerState.value = PlayerState.ERROR } } catch (e: Exception) { Log.e("LofiViewModel", "Server play error", e) _error.value = "Server connection failed: ${e.message}" _playerState.value = PlayerState.ERROR } finally { _isLoading.value = false } } } private fun playViaDirect(channel: Channel) { _isLoading.value = true viewModelScope.launch { try { val stream = withContext(Dispatchers.IO) { YouTubeExtractor.extractAudioStream(channel.videoId!!) } if (stream != null) { audioPlayer.play(stream.videoId, stream.url, stream.streamType) _currentChannel.value = channel _playerState.value = PlayerState.PLAYING } else { _error.value = "Could not extract stream from YouTube" _playerState.value = PlayerState.ERROR } } catch (e: Exception) { Log.e("LofiViewModel", "Direct play error", e) _error.value = "Extraction failed: ${e.message}" _playerState.value = PlayerState.ERROR } finally { _isLoading.value = false } } } fun playNext() { val current = _currentChannel.value ?: return val visible = getVisibleChannels() val currentIndex = visible.indexOfFirst { it.id == current.id } val nextIndex = (currentIndex + 1) % visible.size val nextChannel = visible[nextIndex] if (nextChannel.isLive) { playChannel(nextChannel) } } fun playPrevious() { val current = _currentChannel.value ?: return val visible = getVisibleChannels() val currentIndex = visible.indexOfFirst { it.id == current.id } val prevIndex = if (currentIndex == 0) visible.size - 1 else currentIndex - 1 val prevChannel = visible[prevIndex] if (prevChannel.isLive) { playChannel(prevChannel) } } fun pausePlayback() { audioPlayer.pause() } fun resumePlayback() { audioPlayer.resume() } fun stopPlayback() { audioPlayer.stop() _currentChannel.value = null _playerState.value = PlayerState.IDLE } fun setVolume(volume: Float) { audioPlayer.setVolume(volume) } fun retry() { _error.value = null val channel = _currentChannel.value if (channel != null) { playChannel(channel) } } fun discoverAllChannels() { _isDiscovering.value = true viewModelScope.launch { val mode = Preferences.getStreamingMode(context) val apiKey = Preferences.getYouTubeApiKey(context) val updated = mutableListOf() for (channel in _channelList.toList()) { try { val newChannel = when (mode) { Preferences.StreamingMode.SERVER -> { withContext(Dispatchers.IO) { serverApi.checkChannelLive(channel.id) } }.let { live -> channel.copy( isLive = live?.isLive ?: false, videoId = live?.videoId, liveThumbnail = live?.thumbnail ) } Preferences.StreamingMode.DIRECT -> { withContext(Dispatchers.IO) { YouTubeExtractor.discoverVideo(channel.id, channel.handle, apiKey) } }.let { stream -> channel.copy( isLive = stream != null, videoId = stream?.videoId ) } } updated.add(newChannel) } catch (e: Exception) { Log.e("LofiViewModel", "Discovery error for ${channel.name}", e) updated.add(channel) } delay(500) } _channelList.clear() _channelList.addAll(updated) notifyChannelsChanged() _isDiscovering.value = false } } fun getStreamingMode(): Preferences.StreamingMode { return Preferences.getStreamingMode(context) } fun releaseAudioPlayer() { audioPlayer.release() } override fun onCleared() { } }