mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: convert PlayableMedia to sealed class with specific media types
This commit is contained in:
@@ -3,10 +3,34 @@ package hu.bbara.purefin.model
|
|||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class PlayableMedia (
|
sealed class PlayableMedia {
|
||||||
val id: UUID,
|
abstract val id: UUID
|
||||||
val resumePositionMs: Float,
|
abstract val resumePositionMs: Long
|
||||||
val mediaItem: MediaItem,
|
abstract val mediaItem: MediaItem
|
||||||
val preferences: MediaTrackPreferences,
|
abstract val preferences: MediaTrackPreferences
|
||||||
val mediaSegments: List<MediaSegment>
|
abstract val mediaSegments: List<MediaSegment>
|
||||||
)
|
|
||||||
|
data class Movie(
|
||||||
|
override val id: UUID,
|
||||||
|
override val resumePositionMs: Long,
|
||||||
|
override val mediaItem: MediaItem,
|
||||||
|
override val preferences: MediaTrackPreferences,
|
||||||
|
override val mediaSegments: List<MediaSegment>
|
||||||
|
) : PlayableMedia()
|
||||||
|
|
||||||
|
data class Series(
|
||||||
|
override val id: UUID,
|
||||||
|
override val resumePositionMs: Long,
|
||||||
|
override val mediaItem: MediaItem,
|
||||||
|
override val preferences: MediaTrackPreferences,
|
||||||
|
override val mediaSegments: List<MediaSegment>
|
||||||
|
) : PlayableMedia()
|
||||||
|
|
||||||
|
data class Episode(
|
||||||
|
override val id: UUID,
|
||||||
|
override val resumePositionMs: Long,
|
||||||
|
override val mediaItem: MediaItem,
|
||||||
|
override val preferences: MediaTrackPreferences,
|
||||||
|
override val mediaSegments: List<MediaSegment>
|
||||||
|
) : PlayableMedia()
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,8 +102,14 @@ 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 ->
|
||||||
|
// Only updatePlaylist when episodes are being played. First item is handled when playMedia is being called first time.
|
||||||
|
if (currentMedia is PlayableMedia.Episode) {
|
||||||
updatePlaylist()
|
updatePlaylist()
|
||||||
}
|
}
|
||||||
|
seekTo(currentMedia.resumePositionMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import hu.bbara.purefin.data.MediaCatalogReader
|
import hu.bbara.purefin.data.MediaCatalogReader
|
||||||
import hu.bbara.purefin.image.ArtworkKind
|
import hu.bbara.purefin.image.ArtworkKind
|
||||||
import hu.bbara.purefin.image.ImageUrlBuilder
|
import hu.bbara.purefin.image.ImageUrlBuilder
|
||||||
|
import hu.bbara.purefin.model.PlayableMedia
|
||||||
import hu.bbara.purefin.player.manager.PlayerManager
|
import hu.bbara.purefin.player.manager.PlayerManager
|
||||||
import hu.bbara.purefin.player.manager.ProgressManager
|
import hu.bbara.purefin.player.manager.ProgressManager
|
||||||
import hu.bbara.purefin.player.model.PlayerUiState
|
import hu.bbara.purefin.player.model.PlayerUiState
|
||||||
@@ -122,22 +123,10 @@ class PlayerViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val currentPlayableMedia = playerManager.currentPlayableMedia
|
val currentPlayableMedia = playerManager.currentPlayableMedia
|
||||||
playerManager.playlist.collect { playlist ->
|
playerManager.playlist.collect { playlist ->
|
||||||
val episodes =
|
|
||||||
playlist.map { playableMedia -> mediaCatalogReader.getEpisode(playableMedia.id) }
|
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
queue = episodes.mapNotNull { episode ->
|
queue = playlist.mapNotNull { playableMedia ->
|
||||||
val episodeValue = episode.first()
|
playableMedia.toPlaylistElementUiModel(currentPlayableMedia.value?.id)
|
||||||
if (episodeValue == null) {
|
|
||||||
Log.e("PlayerViewModel", "Episode not found for playlist: $playlist")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
PlaylistElementUiModel(
|
|
||||||
id = episodeValue.id.toString(),
|
|
||||||
title = episodeValue.title,
|
|
||||||
artworkUrl = ImageUrlBuilder.finishImageUrl(prefixImageUrl = episodeValue.imageUrlPrefix, artworkKind = ArtworkKind.PRIMARY),
|
|
||||||
isCurrent = currentPlayableMedia.value?.id == episodeValue.id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,6 +260,64 @@ class PlayerViewModel @Inject constructor(
|
|||||||
_uiState.update { it.copy(error = null) }
|
_uiState.update { it.copy(error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun PlayableMedia.toPlaylistElementUiModel(currentMediaId: UUID?): PlaylistElementUiModel? {
|
||||||
|
return when (this) {
|
||||||
|
is PlayableMedia.Movie -> {
|
||||||
|
val movie = mediaCatalogReader.getMovie(id).first()
|
||||||
|
if (movie == null) {
|
||||||
|
Log.e("PlayerViewModel", "Movie not found for playlist item: $id")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
PlaylistElementUiModel(
|
||||||
|
id = movie.id.toString(),
|
||||||
|
title = movie.title,
|
||||||
|
artworkUrl = ImageUrlBuilder.finishImageUrl(
|
||||||
|
prefixImageUrl = movie.imageUrlPrefix,
|
||||||
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
|
),
|
||||||
|
isCurrent = currentMediaId == movie.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayableMedia.Series -> {
|
||||||
|
val series = mediaCatalogReader.getSeries(id).first()
|
||||||
|
if (series == null) {
|
||||||
|
Log.e("PlayerViewModel", "Series not found for playlist item: $id")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
PlaylistElementUiModel(
|
||||||
|
id = series.id.toString(),
|
||||||
|
title = series.name,
|
||||||
|
artworkUrl = ImageUrlBuilder.finishImageUrl(
|
||||||
|
prefixImageUrl = series.imageUrlPrefix,
|
||||||
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
|
),
|
||||||
|
isCurrent = currentMediaId == series.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayableMedia.Episode -> {
|
||||||
|
val episode = mediaCatalogReader.getEpisode(id).first()
|
||||||
|
if (episode == null) {
|
||||||
|
Log.e("PlayerViewModel", "Episode not found for playlist item: $id")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
PlaylistElementUiModel(
|
||||||
|
id = episode.id.toString(),
|
||||||
|
title = episode.title,
|
||||||
|
artworkUrl = ImageUrlBuilder.finishImageUrl(
|
||||||
|
prefixImageUrl = episode.imageUrlPrefix,
|
||||||
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
|
),
|
||||||
|
isCurrent = currentMediaId == episode.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
autoHideJob?.cancel()
|
autoHideJob?.cancel()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package hu.bbara.purefin.data.catalog
|
package hu.bbara.purefin.data.catalog
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import hu.bbara.purefin.data.MediaRepository
|
import hu.bbara.purefin.data.MediaRepository
|
||||||
import hu.bbara.purefin.data.UserSessionRepository
|
import hu.bbara.purefin.data.UserSessionRepository
|
||||||
import hu.bbara.purefin.data.converter.toEpisode
|
import hu.bbara.purefin.data.converter.toEpisode
|
||||||
@@ -18,9 +19,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -47,6 +50,10 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
override suspend fun getMovie(id: UUID): Flow<Movie?> {
|
override suspend fun getMovie(id: UUID): Flow<Movie?> {
|
||||||
if (!moviesState.value.containsKey(id)) {
|
if (!moviesState.value.containsKey(id)) {
|
||||||
jellyfinApiClient.getItemInfo(id)?.let { item ->
|
jellyfinApiClient.getItemInfo(id)?.let { item ->
|
||||||
|
if (item.type != BaseItemKind.MOVIE) {
|
||||||
|
Log.d("InMemoryMediaRepository", "Item is not an movie: ${item.type}")
|
||||||
|
return flowOf(null)
|
||||||
|
}
|
||||||
val movie = item.toMovie(serverUrl.first())
|
val movie = item.toMovie(serverUrl.first())
|
||||||
moviesState.update { current -> current + (movie.id to movie) }
|
moviesState.update { current -> current + (movie.id to movie) }
|
||||||
}
|
}
|
||||||
@@ -57,6 +64,10 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
override suspend fun getSeries(id: UUID): Flow<Series?> {
|
override suspend fun getSeries(id: UUID): Flow<Series?> {
|
||||||
if (!seriesState.value.containsKey(id)) {
|
if (!seriesState.value.containsKey(id)) {
|
||||||
jellyfinApiClient.getItemInfo(id)?.let { item ->
|
jellyfinApiClient.getItemInfo(id)?.let { item ->
|
||||||
|
if (item.type != BaseItemKind.SERIES) {
|
||||||
|
Log.d("InMemoryMediaRepository", "Item is not an series: ${item.type}")
|
||||||
|
return flowOf(null)
|
||||||
|
}
|
||||||
val series = item.toSeries(serverUrl.first())
|
val series = item.toSeries(serverUrl.first())
|
||||||
seriesState.update { current -> current + (series.id to series) }
|
seriesState.update { current -> current + (series.id to series) }
|
||||||
}
|
}
|
||||||
@@ -67,6 +78,10 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
override suspend fun getEpisode(id: UUID): Flow<Episode?> {
|
override suspend fun getEpisode(id: UUID): Flow<Episode?> {
|
||||||
if (!episodesState.value.containsKey(id)) {
|
if (!episodesState.value.containsKey(id)) {
|
||||||
jellyfinApiClient.getItemInfo(id)?.let { item ->
|
jellyfinApiClient.getItemInfo(id)?.let { item ->
|
||||||
|
if (item.type != BaseItemKind.EPISODE) {
|
||||||
|
Log.d("InMemoryMediaRepository", "Item is not an episode: ${item.type}")
|
||||||
|
return flowOf(null)
|
||||||
|
}
|
||||||
val episode = item.toEpisode(serverUrl.first())
|
val episode = item.toEpisode(serverUrl.first())
|
||||||
episodesState.update { current -> current + (episode.id to episode) }
|
episodesState.update { current -> current + (episode.id to episode) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentType.INTRO
|
import org.jellyfin.sdk.model.api.MediaSegmentType.INTRO
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentType.OUTRO
|
import org.jellyfin.sdk.model.api.MediaSegmentType.OUTRO
|
||||||
@@ -49,13 +50,30 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
val resumePositionMs = calculateResumePosition(baseItem, playbackDecision.mediaSource)
|
val resumePositionMs = calculateResumePosition(baseItem, playbackDecision.mediaSource)
|
||||||
val mediaTrackPreferences = trackPreferencesRepository.getMediaPreferences(mediaId.toString()).first()
|
val mediaTrackPreferences = trackPreferencesRepository.getMediaPreferences(mediaId.toString()).first()
|
||||||
val mediaSegments = getMediaSegments(mediaId)
|
val mediaSegments = getMediaSegments(mediaId)
|
||||||
PlayableMedia(
|
when (baseItem.type) {
|
||||||
|
BaseItemKind.MOVIE -> PlayableMedia.Movie(
|
||||||
id = mediaId,
|
id = mediaId,
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
resumePositionMs = resumePositionMs?.toFloat() ?: 0f,
|
resumePositionMs = resumePositionMs ?: 0L,
|
||||||
preferences = mediaTrackPreferences,
|
preferences = mediaTrackPreferences,
|
||||||
mediaSegments = mediaSegments
|
mediaSegments = mediaSegments
|
||||||
)
|
)
|
||||||
|
BaseItemKind.SERIES -> PlayableMedia.Series(
|
||||||
|
id = mediaId,
|
||||||
|
mediaItem = mediaItem,
|
||||||
|
resumePositionMs = resumePositionMs ?: 0L,
|
||||||
|
preferences = mediaTrackPreferences,
|
||||||
|
mediaSegments = mediaSegments
|
||||||
|
)
|
||||||
|
BaseItemKind.EPISODE -> PlayableMedia.Episode(
|
||||||
|
id = mediaId,
|
||||||
|
mediaItem = mediaItem,
|
||||||
|
resumePositionMs = resumePositionMs ?: 0L,
|
||||||
|
preferences = mediaTrackPreferences,
|
||||||
|
mediaSegments = mediaSegments
|
||||||
|
)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getMediaItem(baseItem: BaseItemDto, playbackDecision: PlaybackDecision): MediaItem = withContext(Dispatchers.IO) {
|
private suspend fun getMediaItem(baseItem: BaseItemDto, playbackDecision: PlaybackDecision): MediaItem = withContext(Dispatchers.IO) {
|
||||||
|
|||||||
Reference in New Issue
Block a user