refactor: update media retrieval to use Flow and improve error handling in PlaylistQueue loading

This commit is contained in:
2026-04-25 22:31:39 +02:00
parent b15453ab01
commit 81a3504606
4 changed files with 40 additions and 42 deletions

View File

@@ -46,19 +46,16 @@ class CompositeMediaRepository @Inject constructor(
override suspend fun getMovie(id: UUID): Flow<Movie?> { override suspend fun getMovie(id: UUID): Flow<Movie?> {
return activeRepository return activeRepository
.flatMapLatest { it.getMovie(id) } .flatMapLatest { it.getMovie(id) }
.stateIn(scope, SharingStarted.Companion.Eagerly, null)
} }
override suspend fun getSeries(id: UUID): Flow<Series?> { override suspend fun getSeries(id: UUID): Flow<Series?> {
return activeRepository return activeRepository
.flatMapLatest { it.getSeries(id) } .flatMapLatest { it.getSeries(id) }
.stateIn(scope, SharingStarted.Companion.Eagerly, null)
} }
override suspend fun getEpisode(id: UUID): Flow<Episode?> { override suspend fun getEpisode(id: UUID): Flow<Episode?> {
return activeRepository return activeRepository
.flatMapLatest { it.getEpisode(id) } .flatMapLatest { it.getEpisode(id) }
.stateIn(scope, SharingStarted.Companion.Eagerly, null)
} }
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> { override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {

View File

@@ -27,12 +27,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -61,28 +61,27 @@ class PlayerManager @Inject constructor(
private val mediaSegmentManager = MediaSegmentManager(player as ExoPlayer) private val mediaSegmentManager = MediaSegmentManager(player as ExoPlayer)
private val _currentMediaId = MutableStateFlow<UUID?>(null) private val _currentMediaId = MutableStateFlow<UUID?>(null)
val currentPlayableMedia: StateFlow<PlayableMedia?> by lazy { private val _playlist = MutableStateFlow(emptyList<PlayableMedia>())
val playlist: StateFlow<List<PlayableMedia>> = _playlist.asStateFlow()
val currentPlayableMedia: Flow<PlayableMedia?> =
combine(_currentMediaId, _playlist) { mediaId, playlist -> combine(_currentMediaId, _playlist) { mediaId, playlist ->
playlist.firstOrNull { it.id == mediaId } playlist.firstOrNull { it.id == mediaId }
}.stateIn(scope, SharingStarted.Eagerly, null)
} }
private var pendingSeekPositionMs: Long? = null private var pendingSeekPositionMs: Long? = null
private val _playbackState = MutableStateFlow(PlaybackStateSnapshot()) private val _playbackState = MutableStateFlow(PlaybackStateSnapshot())
val playbackState: StateFlow<PlaybackStateSnapshot> = _playbackState.asStateFlow() val playbackState: StateFlow<PlaybackStateSnapshot> = _playbackState.asStateFlow()
private val _progress = MutableStateFlow(PlaybackProgressSnapshot()) private val _progress = MutableStateFlow(PlaybackProgressSnapshot())
val progress: StateFlow<PlaybackProgressSnapshot> = _progress.asStateFlow() val progress: StateFlow<PlaybackProgressSnapshot> = _progress.asStateFlow()
private val _metadata = MutableStateFlow(MetadataState()) private val _metadata = MutableStateFlow(MetadataState())
val metadata: StateFlow<MetadataState> = _metadata.asStateFlow() val metadata: StateFlow<MetadataState> = _metadata.asStateFlow()
private val _tracks = MutableStateFlow(TrackSelectionState()) private val _tracks = MutableStateFlow(TrackSelectionState())
val tracks: StateFlow<TrackSelectionState> = _tracks.asStateFlow()
private val _playlist = MutableStateFlow(emptyList<PlayableMedia>()) val tracks: StateFlow<TrackSelectionState> = _tracks.asStateFlow()
val playlist: StateFlow<List<PlayableMedia>> = _playlist.asStateFlow()
private val listener = object : Player.Listener { private val listener = object : Player.Listener {
override fun onPositionDiscontinuity( override fun onPositionDiscontinuity(
@@ -102,13 +101,17 @@ class PlayerManager @Inject constructor(
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
_currentMediaId.value = mediaItem?.mediaId?.let { UUID.fromString(it) } _currentMediaId.value = mediaItem?.mediaId?.let { UUID.fromString(it) }
scope.launch { scope.launch {
currentPlayableMedia.value?.let { currentMedia -> val currentMedia = _playlist.value.firstOrNull { it.id == _currentMediaId.value }
if (currentMedia == null) {
_playbackState.update { it.copy(error = "Media not found in playlist") }
return@launch
}
// Only updatePlaylist when episodes are being played. First item is handled when playMedia is being called first time. // Only updatePlaylist when episodes are being played. First item is handled when playMedia is being called first time.
if (currentMedia is PlayableMedia.Episode) { if (currentMedia is PlayableMedia.Episode) {
updatePlaylist() updatePlaylist()
} }
seekTo(currentMedia.resumePositionMs) seekTo(currentMedia.resumePositionMs)
} resumePlayback()
} }
} }
@@ -142,21 +145,20 @@ class PlayerManager @Inject constructor(
val index = _playlist.value.indexOfFirst { it.id == mediaId } val index = _playlist.value.indexOfFirst { it.id == mediaId }
if (index != -1) { if (index != -1) {
player.seekToDefaultPosition(index) player.seekToDefaultPosition(index)
player.playWhenReady = true
} else { } else {
_playbackState.update { it.copy(error = "Media not found in playlist") } _playbackState.update { it.copy(error = "Media not found in playlist") }
} }
} }
private suspend fun playNewMedia(mediaId: UUID) { private suspend fun playNewMedia(mediaId: UUID) {
val playableMedia = playableMediaRepository.getPlayableMedia(mediaId) var playableMedia = playableMediaRepository.getPlayableMedia(mediaId)
if (playableMedia == null) { if (playableMedia == null) {
_playbackState.update { it.copy(error = "Media not found") } _playbackState.update { it.copy(error = "Media not found") }
return return
} }
_playlist.update { it + playableMedia }
player.setMediaItem(playableMedia.mediaItem) player.setMediaItem(playableMedia.mediaItem)
player.prepare() player.prepare()
player.playWhenReady = true
} }
private suspend fun updatePlaylist() { private suspend fun updatePlaylist() {
@@ -289,8 +291,8 @@ class PlayerManager @Inject constructor(
_playbackState.update { it.copy(error = null) } _playbackState.update { it.copy(error = null) }
} }
private fun applyTrackPreferences() { private suspend fun applyTrackPreferences() {
val preferences = currentPlayableMedia.value?.preferences ?: return val preferences = currentPlayableMedia.firstOrNull()?.preferences ?: return
val currentTrackState = _tracks.value val currentTrackState = _tracks.value

View File

@@ -126,7 +126,7 @@ class PlayerViewModel @Inject constructor(
_uiState.update { state -> _uiState.update { state ->
state.copy( state.copy(
queue = playlist.mapNotNull { playableMedia -> queue = playlist.mapNotNull { playableMedia ->
playableMedia.toPlaylistElementUiModel(currentPlayableMedia.value?.id) playableMedia.toPlaylistElementUiModel(currentPlayableMedia.first()?.id)
} }
) )
} }

View File

@@ -86,10 +86,9 @@ class InMemoryMediaRepository @Inject constructor(
episodesState.update { current -> current + (episode.id to episode) } episodesState.update { current -> current + (episode.id to episode) }
} }
} }
val episodeFlow = episodesState.map { it[id] } val episode = episodesState.value[id] ?: return flowOf(null)
val seriesId = episodeFlow.first()!!.seriesId observeSeriesWithContent(seriesId = episode.seriesId)
observeSeriesWithContent(seriesId = seriesId) return episodesState.map { it[id] }
return episodeFlow
} }
fun upsertMovies(movies: List<Movie>) { fun upsertMovies(movies: List<Movie>) {