mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: centralize playable media state
Move playback-related media state into shared model types and make the player consume a single PlayableMedia object instead of separately loading MediaItems, track preferences, resume positions, and media segments. This updates PlayableMediaRepository to return playable media bundles, seeds the player playlist with the current item, keeps next-up entries in the same model, restores resume/segment handling, and fixes current queue highlighting. The change also moves track preference models into core-model, centralizes Jellyfin BaseItemDto conversion, removes placeholder movie audio/subtitle fields, bumps the offline Room schema, and updates mobile/TV queue UI code for the playlist model. Verification: ./gradlew --no-daemon :core-model:compileDebugKotlin :core:compileDebugKotlin :data:compileDebugKotlin :app:compileDebugKotlin
This commit is contained in:
@@ -43,6 +43,24 @@ class CompositeMediaRepository @Inject constructor(
|
||||
.flatMapLatest { it.episodes }
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyMap())
|
||||
|
||||
override suspend fun getMovie(id: UUID): Flow<Movie?> {
|
||||
return activeRepository
|
||||
.flatMapLatest { it.getMovie(id) }
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, null)
|
||||
}
|
||||
|
||||
override suspend fun getSeries(id: UUID): Flow<Series?> {
|
||||
return activeRepository
|
||||
.flatMapLatest { it.getSeries(id) }
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, null)
|
||||
}
|
||||
|
||||
override suspend fun getEpisode(id: UUID): Flow<Episode?> {
|
||||
return activeRepository
|
||||
.flatMapLatest { it.getEpisode(id) }
|
||||
.stateIn(scope, SharingStarted.Companion.Eagerly, null)
|
||||
}
|
||||
|
||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||
return activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) }
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ interface MediaCatalogReader {
|
||||
val movies: StateFlow<Map<UUID, Movie>>
|
||||
val series: StateFlow<Map<UUID, Series>>
|
||||
val episodes: StateFlow<Map<UUID, Episode>>
|
||||
suspend fun getMovie(id: UUID): Flow<Movie?>
|
||||
suspend fun getSeries(id: UUID): Flow<Series?>
|
||||
suspend fun getEpisode(id: UUID): Flow<Episode?>
|
||||
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package hu.bbara.purefin.data
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import hu.bbara.purefin.model.MediaSegment
|
||||
import hu.bbara.purefin.model.PlayableMedia
|
||||
import java.util.UUID
|
||||
|
||||
interface PlayableMediaRepository {
|
||||
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>?
|
||||
suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment>
|
||||
suspend fun getNextUpMediaItems(
|
||||
suspend fun getPlayableMedia(mediaId: UUID): PlayableMedia?
|
||||
suspend fun getNextUpPlayableMedias(
|
||||
episodeId: UUID,
|
||||
existingIds: Set<String>,
|
||||
count: Int = 9,
|
||||
): List<MediaItem>
|
||||
existingIds: Set<UUID>,
|
||||
count: Int,
|
||||
): List<PlayableMedia>
|
||||
}
|
||||
|
||||
@@ -9,17 +9,17 @@ import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.data.PlayableMediaRepository
|
||||
import hu.bbara.purefin.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.model.AudioTrackProperties
|
||||
import hu.bbara.purefin.model.MediaSegment
|
||||
import hu.bbara.purefin.player.model.MediaContext
|
||||
import hu.bbara.purefin.model.PlayableMedia
|
||||
import hu.bbara.purefin.model.SubtitleTrackProperties
|
||||
import hu.bbara.purefin.player.model.MetadataState
|
||||
import hu.bbara.purefin.player.model.PlaybackProgressSnapshot
|
||||
import hu.bbara.purefin.player.model.PlaybackStateSnapshot
|
||||
import hu.bbara.purefin.player.model.QueueItemUi
|
||||
import hu.bbara.purefin.player.model.TrackOption
|
||||
import hu.bbara.purefin.player.model.TrackType
|
||||
import hu.bbara.purefin.player.preference.AudioTrackProperties
|
||||
import hu.bbara.purefin.player.preference.SubtitleTrackProperties
|
||||
import hu.bbara.purefin.player.preference.TrackMatcher
|
||||
import hu.bbara.purefin.player.preference.TrackPreferencesRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -28,11 +28,15 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -44,6 +48,7 @@ import kotlin.math.abs
|
||||
class PlayerManager @Inject constructor(
|
||||
val player: Player,
|
||||
private val trackMapper: TrackMapper,
|
||||
private val playableMediaRepository: PlayableMediaRepository,
|
||||
private val trackPreferencesRepository: TrackPreferencesRepository,
|
||||
private val trackMatcher: TrackMatcher
|
||||
) {
|
||||
@@ -55,7 +60,13 @@ class PlayerManager @Inject constructor(
|
||||
|
||||
private val mediaSegmentManager = MediaSegmentManager(player as ExoPlayer)
|
||||
|
||||
private var currentMediaContext: MediaContext? = null
|
||||
private val _currentMediaId = MutableStateFlow<UUID?>(null)
|
||||
val currentPlayableMedia: StateFlow<PlayableMedia?> by lazy {
|
||||
combine(_currentMediaId, _playlist) { mediaId, playlist ->
|
||||
playlist.firstOrNull { it.id == mediaId }
|
||||
}.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
}
|
||||
|
||||
private var pendingSeekPositionMs: Long? = null
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackStateSnapshot())
|
||||
@@ -70,8 +81,8 @@ class PlayerManager @Inject constructor(
|
||||
private val _tracks = MutableStateFlow(TrackSelectionState())
|
||||
val tracks: StateFlow<TrackSelectionState> = _tracks.asStateFlow()
|
||||
|
||||
private val _queue = MutableStateFlow<List<QueueItemUi>>(emptyList())
|
||||
val queue: StateFlow<List<QueueItemUi>> = _queue.asStateFlow()
|
||||
private val _playlist = MutableStateFlow(emptyList<PlayableMedia>())
|
||||
val playlist: StateFlow<List<PlayableMedia>> = _playlist.asStateFlow()
|
||||
|
||||
private val listener = object : Player.Listener {
|
||||
override fun onPositionDiscontinuity(
|
||||
@@ -89,9 +100,9 @@ class PlayerManager @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
clearPendingSeek()
|
||||
currentMediaContext?.let {
|
||||
installMediaSegments(it.mediaSegments)
|
||||
_currentMediaId.value = mediaItem?.mediaId?.let { UUID.fromString(it) }
|
||||
scope.launch {
|
||||
updatePlaylist()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,16 +122,49 @@ class PlayerManager @Inject constructor(
|
||||
startProgressLoop()
|
||||
}
|
||||
|
||||
fun play(mediaItem: MediaItem, mediaContext: MediaContext? = null) {
|
||||
currentMediaContext = mediaContext
|
||||
clearPendingSeek()
|
||||
player.setMediaItem(mediaItem)
|
||||
fun play(mediaId: UUID) {
|
||||
scope.launch {
|
||||
if (_playlist.value.map { it.id }.contains(mediaId)) {
|
||||
playFromPlaylist(mediaId)
|
||||
} else {
|
||||
playNewMedia(mediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playFromPlaylist(mediaId: UUID) {
|
||||
val index = _playlist.value.indexOfFirst { it.id == mediaId }
|
||||
if (index != -1) {
|
||||
player.seekToDefaultPosition(index)
|
||||
player.playWhenReady = true
|
||||
} else {
|
||||
_playbackState.update { it.copy(error = "Media not found in playlist") }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun playNewMedia(mediaId: UUID) {
|
||||
val playableMedia = playableMediaRepository.getPlayableMedia(mediaId)
|
||||
if (playableMedia == null) {
|
||||
_playbackState.update { it.copy(error = "Media not found") }
|
||||
return
|
||||
}
|
||||
player.setMediaItem(playableMedia.mediaItem)
|
||||
player.prepare()
|
||||
player.playWhenReady = true
|
||||
}
|
||||
|
||||
fun addToQueue(mediaItem: MediaItem) {
|
||||
player.addMediaItem(mediaItem)
|
||||
private suspend fun updatePlaylist() {
|
||||
val nextUpPlayableMedias = playableMediaRepository.getNextUpPlayableMedias(
|
||||
episodeId = _currentMediaId.value ?: return,
|
||||
existingIds = _playlist.value.map { it.id }.toSet(),
|
||||
count = 5
|
||||
)
|
||||
addToQueue(nextUpPlayableMedias)
|
||||
}
|
||||
|
||||
private fun addToQueue(playableMedias: List<PlayableMedia>) {
|
||||
_playlist.update { it + playableMedias }
|
||||
player.addMediaItems(playableMedias.map { it.mediaItem })
|
||||
}
|
||||
|
||||
fun togglePlayPause() {
|
||||
@@ -215,10 +259,10 @@ class PlayerManager @Inject constructor(
|
||||
}
|
||||
player.trackSelectionParameters = builder.build()
|
||||
|
||||
// Save track preference if media context is available
|
||||
currentMediaContext?.let { context ->
|
||||
// Save track preference if media id is available
|
||||
_currentMediaId.value?.let { mediaId ->
|
||||
scope.launch {
|
||||
saveTrackPreference(option, context.mediaId)
|
||||
saveTrackPreference(option, mediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,23 +279,12 @@ class PlayerManager @Inject constructor(
|
||||
player.playWhenReady = true
|
||||
}
|
||||
|
||||
fun playQueueItem(id: String) {
|
||||
val items = _queue.value
|
||||
val targetIndex = items.indexOfFirst { it.id == id }
|
||||
if (targetIndex >= 0) {
|
||||
clearPendingSeek()
|
||||
player.seekToDefaultPosition(targetIndex)
|
||||
player.playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_playbackState.update { it.copy(error = null) }
|
||||
}
|
||||
|
||||
private fun applyTrackPreferences() {
|
||||
val context = currentMediaContext ?: return
|
||||
val preferences = context.preferences
|
||||
val preferences = currentPlayableMedia.value?.preferences ?: return
|
||||
|
||||
val currentTrackState = _tracks.value
|
||||
|
||||
@@ -274,7 +307,7 @@ class PlayerManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveTrackPreference(option: TrackOption, preferenceKey: String) {
|
||||
private suspend fun saveTrackPreference(option: TrackOption, mediaId: UUID) {
|
||||
when (option.type) {
|
||||
TrackType.AUDIO -> {
|
||||
val properties = AudioTrackProperties(
|
||||
@@ -282,7 +315,7 @@ class PlayerManager @Inject constructor(
|
||||
channelCount = option.channelCount,
|
||||
label = option.label
|
||||
)
|
||||
trackPreferencesRepository.saveAudioPreference(preferenceKey, properties)
|
||||
trackPreferencesRepository.saveAudioPreference(mediaId.toString(), properties)
|
||||
}
|
||||
|
||||
TrackType.TEXT -> {
|
||||
@@ -292,7 +325,7 @@ class PlayerManager @Inject constructor(
|
||||
label = option.label,
|
||||
isOff = option.isOff
|
||||
)
|
||||
trackPreferencesRepository.saveSubtitlePreference(preferenceKey, properties)
|
||||
trackPreferencesRepository.saveSubtitlePreference(mediaId.toString(), properties)
|
||||
}
|
||||
|
||||
TrackType.VIDEO -> {
|
||||
@@ -346,24 +379,6 @@ class PlayerManager @Inject constructor(
|
||||
playbackReportContext = playbackReportContext,
|
||||
)
|
||||
_tracks.value = trackMapper.map(player.currentTracks)
|
||||
_queue.value = buildQueueSnapshot(player)
|
||||
}
|
||||
|
||||
private fun buildQueueSnapshot(player: Player): List<QueueItemUi> {
|
||||
val items = mutableListOf<QueueItemUi>()
|
||||
for (i in 0 until player.mediaItemCount) {
|
||||
val mediaItem = player.getMediaItemAt(i)
|
||||
items.add(
|
||||
QueueItemUi(
|
||||
id = mediaItem.mediaId.ifEmpty { i.toString() },
|
||||
title = mediaItem.mediaMetadata.title?.toString() ?: "Item ${i + 1}",
|
||||
subtitle = mediaItem.mediaMetadata.subtitle?.toString(),
|
||||
artworkUrl = mediaItem.mediaMetadata.artworkUri?.toString(),
|
||||
isCurrent = i == player.currentMediaItemIndex
|
||||
)
|
||||
)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private fun clampSeekPosition(positionMs: Long): Long {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package hu.bbara.purefin.player.model
|
||||
|
||||
import hu.bbara.purefin.model.MediaSegment
|
||||
import hu.bbara.purefin.player.preference.MediaTrackPreferences
|
||||
import hu.bbara.purefin.model.MediaTrackPreferences
|
||||
|
||||
data class MediaContext(
|
||||
val mediaId: String,
|
||||
|
||||
@@ -14,7 +14,7 @@ data class PlayerUiState(
|
||||
val playbackSpeed: Float = 1f,
|
||||
val chapters: List<TimedMarker> = emptyList(),
|
||||
val ads: List<TimedMarker> = emptyList(),
|
||||
val queue: List<QueueItemUi> = emptyList(),
|
||||
val queue: List<PlaylistElementUiModel> = emptyList(),
|
||||
val audioTracks: List<TrackOption> = emptyList(),
|
||||
val textTracks: List<TrackOption> = emptyList(),
|
||||
val qualityTracks: List<TrackOption> = emptyList(),
|
||||
@@ -47,10 +47,9 @@ data class TimedMarker(
|
||||
|
||||
enum class MarkerType { CHAPTER, AD }
|
||||
|
||||
data class QueueItemUi(
|
||||
data class PlaylistElementUiModel(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val artworkUrl: String?,
|
||||
val isCurrent: Boolean
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package hu.bbara.purefin.player.preference
|
||||
|
||||
import hu.bbara.purefin.model.AudioTrackProperties
|
||||
import hu.bbara.purefin.model.SubtitleTrackProperties
|
||||
import hu.bbara.purefin.player.model.TrackOption
|
||||
import hu.bbara.purefin.player.model.TrackType
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package hu.bbara.purefin.player.preference
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TrackPreferences(
|
||||
val mediaPreferences: Map<String, MediaTrackPreferences> = emptyMap()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaTrackPreferences(
|
||||
val mediaId: String,
|
||||
val audioPreference: AudioTrackProperties? = null,
|
||||
val subtitlePreference: SubtitleTrackProperties? = null
|
||||
) {
|
||||
companion object {
|
||||
fun empty(mediaId: String): MediaTrackPreferences {
|
||||
return MediaTrackPreferences(mediaId = mediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AudioTrackProperties(
|
||||
val language: String? = null,
|
||||
val channelCount: Int? = null,
|
||||
val label: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubtitleTrackProperties(
|
||||
val language: String? = null,
|
||||
val forced: Boolean = false,
|
||||
val label: String? = null,
|
||||
val isOff: Boolean = false
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import hu.bbara.purefin.model.TrackPreferences
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package hu.bbara.purefin.player.preference
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import hu.bbara.purefin.model.AudioTrackProperties
|
||||
import hu.bbara.purefin.model.MediaTrackPreferences
|
||||
import hu.bbara.purefin.model.SubtitleTrackProperties
|
||||
import hu.bbara.purefin.model.TrackPreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -2,6 +2,7 @@ package hu.bbara.purefin.player.preference
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import hu.bbara.purefin.model.TrackPreferences
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -4,13 +4,12 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.data.MediaCatalogReader
|
||||
import hu.bbara.purefin.player.manager.PlayerManager
|
||||
import hu.bbara.purefin.player.manager.ProgressManager
|
||||
import hu.bbara.purefin.player.model.MediaContext
|
||||
import hu.bbara.purefin.player.model.PlayerUiState
|
||||
import hu.bbara.purefin.player.model.PlaylistElementUiModel
|
||||
import hu.bbara.purefin.player.model.TrackOption
|
||||
import hu.bbara.purefin.player.preference.TrackPreferencesRepository
|
||||
import hu.bbara.purefin.data.PlayableMediaRepository
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -27,8 +26,7 @@ import javax.inject.Inject
|
||||
class PlayerViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val playerManager: PlayerManager,
|
||||
private val playableMediaRepository: PlayableMediaRepository,
|
||||
private val trackPreferencesRepository: TrackPreferencesRepository,
|
||||
private val mediaCatalogReader: MediaCatalogReader,
|
||||
private val progressManager: ProgressManager,
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
@@ -47,7 +45,6 @@ class PlayerViewModel @Inject constructor(
|
||||
|
||||
private val controlsAutoHidePolicy = ControlsAutoHidePolicy(DEFAULT_CONTROLS_AUTO_HIDE_MS)
|
||||
private var autoHideJob: Job? = null
|
||||
private var lastNextUpMediaId: String? = null
|
||||
private var dataErrorMessage: String? = null
|
||||
|
||||
init {
|
||||
@@ -101,11 +98,6 @@ class PlayerViewModel @Inject constructor(
|
||||
subtitle = metadata.subtitle
|
||||
)
|
||||
}
|
||||
val currentMediaId = metadata.mediaId
|
||||
if (!currentMediaId.isNullOrEmpty() && currentMediaId != lastNextUpMediaId) {
|
||||
lastNextUpMediaId = currentMediaId
|
||||
loadNextUp(currentMediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,76 +117,63 @@ class PlayerViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
playerManager.queue.collect { queue ->
|
||||
_uiState.update { it.copy(queue = queue) }
|
||||
val currentPlayableMedia = playerManager.currentPlayableMedia
|
||||
playerManager.playlist.collect { playlist ->
|
||||
val episodes =
|
||||
playlist.map { playableMedia -> mediaCatalogReader.getEpisode(playableMedia.id) }
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
queue = episodes.mapNotNull { episode ->
|
||||
val episodeValue = episode.first()
|
||||
if (episodeValue == null) {
|
||||
throw IllegalStateException("Episode not found for media id: $episodeValue")
|
||||
}
|
||||
PlaylistElementUiModel(
|
||||
id = episodeValue.id.toString(),
|
||||
title = episodeValue.title,
|
||||
artworkUrl = episodeValue.imageUrlPrefix,
|
||||
isCurrent = currentPlayableMedia.value?.id == episodeValue.id
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInitialMedia() {
|
||||
val id = mediaId ?: return
|
||||
loadMediaById(id)
|
||||
viewModelScope.launch {
|
||||
loadMediaById(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMedia(id: String) {
|
||||
if (mediaId != null) return // Already loading from SavedStateHandle
|
||||
loadMediaById(id)
|
||||
viewModelScope.launch {
|
||||
loadMediaById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
private fun loadMediaById(id: String) {
|
||||
private suspend fun loadMediaById(id: String) {
|
||||
val uuid = id.toUuidOrNull()
|
||||
if (uuid == null) {
|
||||
dataErrorMessage = "Invalid media id"
|
||||
_uiState.update { it.copy(error = dataErrorMessage) }
|
||||
return
|
||||
}
|
||||
//TODO hack to preload the series media
|
||||
mediaCatalogReader.getEpisode(UUID.fromString(id))
|
||||
viewModelScope.launch {
|
||||
val result = playableMediaRepository.getMediaItem(uuid)
|
||||
if (result != null) {
|
||||
val (mediaItem, resumePositionMs) = result
|
||||
|
||||
|
||||
// TODO use CatalogReader for this instead of episodeSeriesLookup
|
||||
// val preferenceKey = episodeSeriesLookup.preferenceKeyFor(uuid)
|
||||
val preferenceKey = uuid.toString()
|
||||
val preferences = trackPreferencesRepository.getMediaPreferences(preferenceKey).first()
|
||||
val mediaSegments = playableMediaRepository.getMediaSegments(uuid)
|
||||
|
||||
val mediaContext = MediaContext(
|
||||
mediaId = id,
|
||||
preferences = preferences,
|
||||
mediaSegments = mediaSegments
|
||||
)
|
||||
|
||||
playerManager.play(mediaItem, mediaContext)
|
||||
|
||||
// Seek to resume position after play() is called
|
||||
resumePositionMs?.let { playerManager.seekTo(it) }
|
||||
|
||||
if (dataErrorMessage != null) {
|
||||
dataErrorMessage = null
|
||||
_uiState.update { it.copy(error = null) }
|
||||
}
|
||||
} else {
|
||||
dataErrorMessage = "Unable to load media"
|
||||
_uiState.update { it.copy(error = dataErrorMessage) }
|
||||
runCatching {
|
||||
playerManager.play(uuid)
|
||||
}.onFailure { e ->
|
||||
_uiState.update { it.copy(error = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadNextUp(currentMediaId: String) {
|
||||
val uuid = currentMediaId.toUuidOrNull() ?: return
|
||||
viewModelScope.launch {
|
||||
val queuedIds = uiState.value.queue.map { it.id }.toSet()
|
||||
val items = playableMediaRepository.getNextUpMediaItems(
|
||||
episodeId = uuid,
|
||||
existingIds = queuedIds,
|
||||
)
|
||||
items.forEach { playerManager.addToQueue(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePlayPause(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||
playerManager.togglePlayPause()
|
||||
showControls(autoHideDelayMs)
|
||||
@@ -277,9 +256,9 @@ class PlayerViewModel @Inject constructor(
|
||||
playerManager.retry()
|
||||
}
|
||||
|
||||
fun playQueueItem(id: String, autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||
playerManager.playQueueItem(id)
|
||||
showControls(autoHideDelayMs)
|
||||
fun playQueueItem(id: String) {
|
||||
playerManager.play(id.toUuidOrNull() ?: return)
|
||||
showControls()
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
|
||||
@@ -66,8 +66,6 @@ class MovieUiModel: MediaUiModel {
|
||||
format = "",
|
||||
synopsis = "",
|
||||
imageUrlPrefix = "",
|
||||
audioTrack = "",
|
||||
subtitles = "",
|
||||
cast = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user