diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt index e6bcb867..e1b6cf1b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt @@ -2,17 +2,30 @@ package com.github.damontecres.wholphin.services import android.content.Context import androidx.annotation.OptIn +import androidx.datastore.core.DataStore import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient +import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope +import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.util.profile.Codec import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.jellyfin.sdk.api.client.ApiClient @@ -34,7 +47,12 @@ class ThemeSongPlayer @param:ApplicationContext private val context: Context, @param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient, private val api: ApiClient, + @param:IoCoroutineScope private val scope: CoroutineScope, + private val preferences: DataStore, ) { + private val mutex = Mutex() + private val state = MutableStateFlow(ThemeSongState()) + private val player: Player by lazy { ExoPlayer .Builder(context) @@ -45,68 +63,86 @@ class ThemeSongPlayer ).build() } - suspend fun playThemeFor( - itemId: UUID, - volume: ThemeSongVolume, - ): Boolean = - withContext(Dispatchers.IO) { - if (volume == ThemeSongVolume.DISABLED || volume == ThemeSongVolume.UNRECOGNIZED) { - return@withContext false - } - val themeSongs by api.libraryApi.getThemeSongs(itemId) - return@withContext themeSongs.items.randomOrNull()?.let { theme -> - val url = - api.universalAudioApi.getUniversalAudioStreamUrl( - theme.id, - container = - listOf( - Codec.Audio.OPUS, - Codec.Audio.MP3, - Codec.Audio.AAC, - Codec.Audio.FLAC, - ), + fun playThemeFor(itemId: UUID) { + scope.launch { + mutex.withLock { + state.value.job?.join() + val volumeLevel = + when ( + preferences.data + .first() + .interfacePreferences.playThemeSongs + ) { + ThemeSongVolume.UNRECOGNIZED, + ThemeSongVolume.DISABLED, + -> return@withLock + + ThemeSongVolume.LOWEST -> .05f + + ThemeSongVolume.LOW -> .1f + + ThemeSongVolume.MEDIUM -> .25f + + ThemeSongVolume.HIGH -> .5f + + ThemeSongVolume.HIGHEST -> 75f + } + val job = + scope.launchIO { + val themeSongs by api.libraryApi.getThemeSongs(itemId) + themeSongs.items.randomOrNull()?.let { theme -> + val url = + api.universalAudioApi.getUniversalAudioStreamUrl( + theme.id, + container = + listOf( + Codec.Audio.OPUS, + Codec.Audio.MP3, + Codec.Audio.AAC, + Codec.Audio.FLAC, + ), + ) + withContext(Dispatchers.Main) { + player.apply { + stop() + volume = volumeLevel + setMediaItem(MediaItem.fromUri(url)) + prepare() + play() + } + } + } + } + state.update { + it.copy( + itemId = itemId, + job = job, ) - Timber.v("Found theme song for $itemId") - withContext(Dispatchers.Main) { - play(volume, url) } - true - } ?: false - } - - private fun play( - volumeLevel: ThemeSongVolume, - url: String, - ) { - val volumeLevel = - when (volumeLevel) { - ThemeSongVolume.UNRECOGNIZED, - ThemeSongVolume.DISABLED, - -> return - - ThemeSongVolume.LOWEST -> .05f - - ThemeSongVolume.LOW -> .1f - - ThemeSongVolume.MEDIUM -> .25f - - ThemeSongVolume.HIGH -> .5f - - ThemeSongVolume.HIGHEST -> 75f } - player.apply { - stop() - volume = volumeLevel - setMediaItem(MediaItem.fromUri(url)) - prepare() - play() } } fun stop() { - if (player.isPlaying) { - Timber.v("Stopping theme song") - player.stop() + scope.launch { + mutex.withLock { + state.value.job?.cancelAndJoin() + withContext(Dispatchers.Main) { + Timber.v("Stopping theme song") + player.stop() + } + state.update { + it.copy( + itemId = null, + job = null, + ) + } + } } } } + +data class ThemeSongState( + val itemId: UUID? = null, + val job: Job? = null, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 8ed6acbe..ea052c99 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -509,15 +509,11 @@ class CollectionFolderViewModel } fun onResumePage() { - viewModelScope.launchIO { + viewModelScope.launchDefault { item.value?.let { Timber.v("onResumePage: %s", loading.value!!::class) if (it.type == BaseItemKind.BOX_SET && loading.value !is DataLoadingState.Error) { - val volume = - userPreferencesService - .getCurrent() - .appPreferences.interfacePreferences.playThemeSongs - themeSongPlayer.playThemeFor(it.id, volume) + themeSongPlayer.playThemeFor(it.id) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt index 915471d7..b2876a09 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt @@ -156,12 +156,7 @@ class CollectionViewModel ) } listenForStateUpdates() - themeSongPlayer.playThemeFor( - itemId, - preferencesService - .getCurrent() - .appPreferences.interfacePreferences.playThemeSongs, - ) + themeSongPlayer.playThemeFor(itemId) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index fa39f919..803e18ce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -132,11 +132,10 @@ fun EpisodeDetails( LoadingState.Success -> { item?.let { ep -> - LifecycleResumeEffect(destination.itemId) { - viewModel.maybePlayThemeSong( - destination.itemId, - preferences.appPreferences.interfacePreferences.playThemeSongs, - ) + LifecycleResumeEffect(ep) { + ep.data.seriesId?.let { seriesId -> + viewModel.maybePlayThemeSong(seriesId) + } onPauseOrDispose { viewModel.release() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt index 2110a3a5..ec09ded1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeViewModel.kt @@ -10,7 +10,6 @@ import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.ItemPlayback -import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.MediaManagementService @@ -178,15 +177,9 @@ class EpisodeViewModel } } - fun maybePlayThemeSong( - seriesId: UUID, - playThemeSongs: ThemeSongVolume, - ) { + fun maybePlayThemeSong(seriesId: UUID) { viewModelScope.launchIO { - themeSongPlayer.playThemeFor(seriesId, playThemeSongs) - addCloseable { - themeSongPlayer.stop() - } + themeSongPlayer.playThemeFor(seriesId) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index e198689d..e249a0d2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -152,10 +152,7 @@ fun MovieDetails( val movie by rememberUpdatedState(s.data) val chosenStreams by rememberUpdatedState(state.chosenStreams) LifecycleResumeEffect(destination.itemId) { - viewModel.maybePlayThemeSong( - destination.itemId, - preferences.appPreferences.interfacePreferences.playThemeSongs, - ) + viewModel.maybePlayThemeSong(destination.itemId) onPauseOrDispose { viewModel.release() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index bd2cc373..1b5ab26e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -13,7 +13,6 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Trailer -import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.ExtrasService import com.github.damontecres.wholphin.services.FavoriteWatchManager @@ -258,12 +257,9 @@ class MovieViewModel } } - fun maybePlayThemeSong( - seriesId: UUID, - playThemeSongs: ThemeSongVolume, - ) { + fun maybePlayThemeSong(itemId: UUID) { viewModelScope.launchIO { - themeSongPlayer.playThemeFor(seriesId, playThemeSongs) + themeSongPlayer.playThemeFor(itemId) addCloseable { themeSongPlayer.stop() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 4d0ce9e2..8852e89b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -282,12 +282,8 @@ class SeriesViewModel fun onResumePage() { item.value?.let { item -> viewModelScope.launchDefault { backdropService.submit(item) } - viewModelScope.launchIO { - val playThemeSongs = - userPreferencesService - .getCurrent() - .appPreferences.interfacePreferences.playThemeSongs - themeSongPlayer.playThemeFor(seriesId, playThemeSongs) + viewModelScope.launchDefault { + themeSongPlayer.playThemeFor(seriesId) } } }