mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat: refactor SeriesScreen and ViewModel for improved season episode loading and download handling
This commit is contained in:
@@ -78,13 +78,14 @@ fun SeriesScreen(
|
|||||||
onDownloadOptionSelected = { option, selectedSeason ->
|
onDownloadOptionSelected = { option, selectedSeason ->
|
||||||
when (option) {
|
when (option) {
|
||||||
SeriesDownloadOption.SEASON ->
|
SeriesDownloadOption.SEASON ->
|
||||||
viewModel.downloadSeason(selectedSeason.episodes)
|
viewModel.downloadSeason(seriesData.id, selectedSeason.id)
|
||||||
SeriesDownloadOption.SERIES ->
|
SeriesDownloadOption.SERIES ->
|
||||||
viewModel.downloadSeries(seriesData)
|
viewModel.downloadSeries(seriesData)
|
||||||
SeriesDownloadOption.SMART ->
|
SeriesDownloadOption.SMART ->
|
||||||
viewModel.enableSmartDownload(seriesData.id)
|
viewModel.enableSmartDownload(seriesData.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onLoadSeasonEpisodes = viewModel::loadSeasonEpisodes,
|
||||||
onObserveSeasonDownloadState = viewModel::observeSeasonDownloadState,
|
onObserveSeasonDownloadState = viewModel::observeSeasonDownloadState,
|
||||||
onBack = viewModel::onGoHome,
|
onBack = viewModel::onGoHome,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -100,6 +101,7 @@ private fun SeriesScreenInternal(
|
|||||||
seriesDownloadState: DownloadState,
|
seriesDownloadState: DownloadState,
|
||||||
seasonDownloadState: DownloadState,
|
seasonDownloadState: DownloadState,
|
||||||
onDownloadOptionSelected: (SeriesDownloadOption, Season) -> Unit,
|
onDownloadOptionSelected: (SeriesDownloadOption, Season) -> Unit,
|
||||||
|
onLoadSeasonEpisodes: (UUID, UUID) -> Unit,
|
||||||
onObserveSeasonDownloadState: (List<Episode>) -> Unit,
|
onObserveSeasonDownloadState: (List<Episode>) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -107,23 +109,17 @@ private fun SeriesScreenInternal(
|
|||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||||
|
|
||||||
fun getDefaultSeason() : Season {
|
fun getDefaultSeason(): Season {
|
||||||
for (season in series.seasons) {
|
return series.seasons.firstOrNull { it.unwatchedEpisodeCount > 0 } ?: series.seasons.first()
|
||||||
val firstUnwatchedEpisode = season.episodes.firstOrNull {
|
|
||||||
it.watched.not()
|
|
||||||
}
|
|
||||||
if (firstUnwatchedEpisode != null) {
|
|
||||||
return season
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return series.seasons.first()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedSeasonId by remember(series.id) { mutableStateOf(getDefaultSeason().id) }
|
var selectedSeasonId by remember(series.id) { mutableStateOf(getDefaultSeason().id) }
|
||||||
val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: getDefaultSeason()
|
val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: getDefaultSeason()
|
||||||
val nextUpEpisode = remember(series) {
|
val nextUpEpisode = selectedSeason.episodes.firstOrNull { !it.watched }
|
||||||
series.seasons.firstNotNullOfOrNull { season ->
|
?: selectedSeason.episodes.firstOrNull()
|
||||||
season.episodes.firstOrNull { !it.watched }
|
|
||||||
} ?: series.seasons.firstOrNull()?.episodes?.firstOrNull()
|
LaunchedEffect(series.id, selectedSeason.id) {
|
||||||
|
onLoadSeasonEpisodes(series.id, selectedSeason.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedSeason.id, selectedSeason.episodes) {
|
LaunchedEffect(selectedSeason.id, selectedSeason.episodes) {
|
||||||
@@ -260,6 +256,7 @@ private fun SeriesScreenPreview() {
|
|||||||
seriesDownloadState = DownloadState.Downloading(progressPercent = 0.58f),
|
seriesDownloadState = DownloadState.Downloading(progressPercent = 0.58f),
|
||||||
seasonDownloadState = DownloadState.NotDownloaded,
|
seasonDownloadState = DownloadState.NotDownloaded,
|
||||||
onDownloadOptionSelected = { _, _ -> },
|
onDownloadOptionSelected = { _, _ -> },
|
||||||
|
onLoadSeasonEpisodes = { _, _ -> },
|
||||||
onObserveSeasonDownloadState = {},
|
onObserveSeasonDownloadState = {},
|
||||||
onBack = {}
|
onBack = {}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -141,15 +141,13 @@ internal fun SeriesActionButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (playAction != null && nextUpEpisode != null) {
|
MediaResumeButton(
|
||||||
MediaResumeButton(
|
text = mediaPlayButtonText(nextUpEpisode?.progress, nextUpEpisode?.watched),
|
||||||
text = mediaPlayButtonText(nextUpEpisode.progress, nextUpEpisode.watched),
|
progress = mediaPlaybackProgress(nextUpEpisode?.progress),
|
||||||
progress = mediaPlaybackProgress(nextUpEpisode.progress),
|
onClick = playAction ?: {},
|
||||||
onClick = playAction,
|
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||||
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
)
|
||||||
)
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
}
|
|
||||||
MediaActionButton(
|
MediaActionButton(
|
||||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
iconColor = MaterialTheme.colorScheme.onSurface,
|
iconColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package hu.bbara.purefin.ui.common.media
|
package hu.bbara.purefin.ui.common.media
|
||||||
|
|
||||||
fun mediaPlayButtonText(progressPercent: Double?, watched: Boolean): String {
|
fun mediaPlayButtonText(progressPercent: Double?, watched: Boolean?): String {
|
||||||
return if ((progressPercent ?: 0.0) > 0.0 && !watched) "Resume" else "Play"
|
return if ((progressPercent ?: 0.0) > 0.0 && watched?.not() == true) "Resume" else "Play"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mediaPlaybackProgress(progressPercent: Double?): Float {
|
fun mediaPlaybackProgress(progressPercent: Double?): Float {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -58,8 +59,12 @@ class CompositeLocalMediaRepository @Inject constructor(
|
|||||||
.flatMapLatest { it.getEpisode(id) }
|
.flatMapLatest { it.getEpisode(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
override suspend fun loadSeasons(seriesId: UUID) {
|
||||||
return activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) }
|
activeRepository.first().loadSeasons(seriesId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||||
|
activeRepository.first().loadSeasonEpisodes(seriesId, seasonId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ interface LocalMediaRepository : LocalMediaUpdater {
|
|||||||
suspend fun getMovie(id: UUID): Flow<Movie?>
|
suspend fun getMovie(id: UUID): Flow<Movie?>
|
||||||
suspend fun getSeries(id: UUID): Flow<Series?>
|
suspend fun getSeries(id: UUID): Flow<Series?>
|
||||||
suspend fun getEpisode(id: UUID): Flow<Episode?>
|
suspend fun getEpisode(id: UUID): Flow<Episode?>
|
||||||
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?>
|
suspend fun loadSeasons(seriesId: UUID)
|
||||||
|
suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ import hu.bbara.purefin.core.navigation.NavigationManager
|
|||||||
import hu.bbara.purefin.core.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.model.Episode
|
import hu.bbara.purefin.model.Episode
|
||||||
import hu.bbara.purefin.model.Series
|
import hu.bbara.purefin.model.Series
|
||||||
import java.util.UUID
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -33,11 +33,12 @@ class SeriesViewModel @Inject constructor(
|
|||||||
private val _seriesId = MutableStateFlow<UUID?>(null)
|
private val _seriesId = MutableStateFlow<UUID?>(null)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val series: StateFlow<Series?> = _seriesId
|
val series: StateFlow<Series?> = combine(
|
||||||
.flatMapLatest { id ->
|
_seriesId,
|
||||||
if (id != null) mediaCatalogReader.observeSeriesWithContent(id) else flowOf(null)
|
mediaCatalogReader.series
|
||||||
}
|
) { seriesId, seriesMap ->
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
seriesId?.let { seriesMap[it] }
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||||
|
|
||||||
private val _seriesDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
private val _seriesDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
||||||
val seriesDownloadState: StateFlow<DownloadState> = _seriesDownloadState
|
val seriesDownloadState: StateFlow<DownloadState> = _seriesDownloadState
|
||||||
@@ -45,8 +46,12 @@ class SeriesViewModel @Inject constructor(
|
|||||||
private val _seasonDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
private val _seasonDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
||||||
val seasonDownloadState: StateFlow<DownloadState> = _seasonDownloadState
|
val seasonDownloadState: StateFlow<DownloadState> = _seasonDownloadState
|
||||||
|
|
||||||
|
private var seasonDownloadStateJob: Job? = null
|
||||||
|
private var seriesDownloadStateJob: Job? = null
|
||||||
|
|
||||||
fun observeSeasonDownloadState(episodes: List<Episode>) {
|
fun observeSeasonDownloadState(episodes: List<Episode>) {
|
||||||
viewModelScope.launch {
|
seasonDownloadStateJob?.cancel()
|
||||||
|
seasonDownloadStateJob = viewModelScope.launch {
|
||||||
if (episodes.isEmpty()) {
|
if (episodes.isEmpty()) {
|
||||||
_seasonDownloadState.value = DownloadState.NotDownloaded
|
_seasonDownloadState.value = DownloadState.NotDownloaded
|
||||||
return@launch
|
return@launch
|
||||||
@@ -58,9 +63,11 @@ class SeriesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun observeSeriesDownloadState(series: Series) {
|
fun observeSeriesDownloadState(series: Series) {
|
||||||
viewModelScope.launch {
|
seriesDownloadStateJob?.cancel()
|
||||||
|
seriesDownloadStateJob = viewModelScope.launch {
|
||||||
val allEpisodes = series.seasons.flatMap { it.episodes }
|
val allEpisodes = series.seasons.flatMap { it.episodes }
|
||||||
if (allEpisodes.isEmpty()) {
|
val hasUnloadedSeasons = series.seasons.any { it.episodes.isEmpty() && it.episodeCount > 0 }
|
||||||
|
if (allEpisodes.isEmpty() || hasUnloadedSeasons) {
|
||||||
_seriesDownloadState.value = DownloadState.NotDownloaded
|
_seriesDownloadState.value = DownloadState.NotDownloaded
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -70,8 +77,21 @@ class SeriesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadSeason(episodes: List<Episode>) {
|
fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
mediaCatalogReader.loadSeasonEpisodes(seriesId, seasonId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadSeason(seriesId: UUID, seasonId: UUID) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
mediaCatalogReader.loadSeasonEpisodes(seriesId, seasonId)
|
||||||
|
val episodes = mediaCatalogReader.getSeries(seriesId)
|
||||||
|
.first()
|
||||||
|
?.seasons
|
||||||
|
?.firstOrNull { it.id == seasonId }
|
||||||
|
?.episodes
|
||||||
|
.orEmpty()
|
||||||
mediaDownloadManager.downloadEpisodes(episodes.map { it.id })
|
mediaDownloadManager.downloadEpisodes(episodes.map { it.id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,11 +102,16 @@ class SeriesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadSeries(series: Series) {
|
fun downloadSeries(seriesData: Series) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val allEpisodeIds = series.seasons.flatMap { season ->
|
seriesData.seasons.forEach { season ->
|
||||||
season.episodes.map { it.id }
|
mediaCatalogReader.loadSeasonEpisodes(seriesData.id, season.id)
|
||||||
}
|
}
|
||||||
|
val allEpisodeIds = mediaCatalogReader.getSeries(seriesData.id)
|
||||||
|
.first()
|
||||||
|
?.seasons
|
||||||
|
.orEmpty()
|
||||||
|
.flatMap { season -> season.episodes.map { it.id } }
|
||||||
mediaDownloadManager.downloadEpisodes(allEpisodeIds)
|
mediaDownloadManager.downloadEpisodes(allEpisodeIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,5 +154,8 @@ class SeriesViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun selectSeries(seriesId: UUID) {
|
fun selectSeries(seriesId: UUID) {
|
||||||
_seriesId.value = seriesId
|
_seriesId.value = seriesId
|
||||||
|
viewModelScope.launch {
|
||||||
|
mediaCatalogReader.loadSeasons(seriesId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.first
|
|||||||
import kotlinx.coroutines.flow.flowOf
|
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 org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -88,7 +87,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val episode = episodesState.value[id] ?: return flowOf(null)
|
val episode = episodesState.value[id] ?: return flowOf(null)
|
||||||
observeSeriesWithContent(seriesId = episode.seriesId)
|
loadSeasons(seriesId = episode.seriesId)
|
||||||
return episodesState.map { it[id] }
|
return episodesState.map { it[id] }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,18 +103,50 @@ class InMemoryLocalMediaRepository @Inject constructor(
|
|||||||
episodesState.update { current -> current + episodes.associateBy { it.id } }
|
episodesState.update { current -> current + episodes.associateBy { it.id } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
override suspend fun loadSeasons(seriesId: UUID) {
|
||||||
scope.launch {
|
try {
|
||||||
try {
|
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
|
||||||
ensureSeriesContentLoaded(seriesId)
|
|
||||||
} catch (error: CancellationException) {
|
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
|
||||||
throw error
|
|
||||||
} catch (error: Exception) {
|
val updatedSeries = series.copy(
|
||||||
Log.e("InMemoryMediaRepository", "Failed to load content for series $seriesId", error)
|
seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
|
||||||
throw error
|
)
|
||||||
}
|
seriesState.update { it + (updatedSeries.id to updatedSeries) }
|
||||||
|
} catch (error: CancellationException) {
|
||||||
|
throw error
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.e("InMemoryMediaRepository", "Failed to load content for series $seriesId", error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
return seriesState.map { it[seriesId] }
|
}
|
||||||
|
|
||||||
|
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||||
|
loadSeasons(seriesId)
|
||||||
|
|
||||||
|
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
|
||||||
|
val season = series.seasons.firstOrNull { it.id == seasonId } ?: return
|
||||||
|
if (season.episodes.isNotEmpty() || season.episodeCount == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverUrl = userSessionRepository.serverUrl.first()
|
||||||
|
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId)
|
||||||
|
.map { it.toEpisode(serverUrl) }
|
||||||
|
seriesState.update { current ->
|
||||||
|
val currentSeries = current[seriesId] ?: return@update current
|
||||||
|
val updatedSeries = currentSeries.copy(
|
||||||
|
seasons = currentSeries.seasons.map {
|
||||||
|
if (it.id == seasonId && it.episodes.isEmpty()) {
|
||||||
|
it.copy(episodes = episodes)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
current + (updatedSeries.id to updatedSeries)
|
||||||
|
}
|
||||||
|
episodesState.update { current -> current + episodes.associateBy { it.id } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
@@ -153,23 +184,4 @@ class InMemoryLocalMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ensureSeriesContentLoaded(seriesId: UUID) {
|
|
||||||
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
|
|
||||||
|
|
||||||
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
|
|
||||||
val serverUrl = userSessionRepository.serverUrl.first()
|
|
||||||
|
|
||||||
val emptySeasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
|
|
||||||
val filledSeasons = emptySeasons.map { season ->
|
|
||||||
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, season.id).map { it.toEpisode(serverUrl) }
|
|
||||||
season.copy(episodes = episodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
val updatedSeries = series.copy(seasons = filledSeasons)
|
|
||||||
seriesState.update { it + (updatedSeries.id to updatedSeries) }
|
|
||||||
|
|
||||||
val allEpisodes = filledSeasons.flatMap { it.episodes }
|
|
||||||
episodesState.update { current -> current + allEpisodes.associateBy { it.id } }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ class OfflineLocalMediaRepository @Inject constructor(
|
|||||||
return episodes.map { it[id] }
|
return episodes.map { it[id] }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
override suspend fun loadSeasons(seriesId: UUID) {
|
||||||
return localDataSource.observeSeriesWithContent(seriesId)
|
// Offline series content is already emitted with its saved seasons.
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||||
|
// Offline series content is already emitted with its saved episodes.
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
|
|||||||
@@ -63,13 +63,11 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
|
|
||||||
private val api = jellyfin.createApi()
|
private val api = jellyfin.createApi()
|
||||||
|
|
||||||
private val itemFields =
|
private val defaultItemFields =
|
||||||
listOf(
|
listOf(
|
||||||
ItemFields.CHILD_COUNT,
|
ItemFields.CHILD_COUNT,
|
||||||
ItemFields.PARENT_ID,
|
ItemFields.PARENT_ID,
|
||||||
ItemFields.DATE_LAST_REFRESHED,
|
// ItemFields.SEASON_USER_DATA,
|
||||||
ItemFields.OVERVIEW,
|
|
||||||
ItemFields.SEASON_USER_DATA,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun getUserId(): UUID? = userSessionRepository.userId.first()
|
private suspend fun getUserId(): UUID? = userSessionRepository.userId.first()
|
||||||
@@ -163,7 +161,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
enableImages = true,
|
enableImages = true,
|
||||||
parentId = libraryId,
|
parentId = libraryId,
|
||||||
fields = itemFields,
|
fields = defaultItemFields + ItemFields.OVERVIEW,
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||||
recursive = true,
|
recursive = true,
|
||||||
@@ -200,7 +198,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
val userId = getUserId() ?: return@logApiFailure emptyList()
|
val userId = getUserId() ?: return@logApiFailure emptyList()
|
||||||
val getResumeItemsRequest = GetResumeItemsRequest(
|
val getResumeItemsRequest = GetResumeItemsRequest(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
fields = itemFields,
|
fields = defaultItemFields + ItemFields.OVERVIEW,
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
startIndex = 0,
|
startIndex = 0,
|
||||||
@@ -218,7 +216,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
}
|
}
|
||||||
val getNextUpRequest = GetNextUpRequest(
|
val getNextUpRequest = GetNextUpRequest(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
fields = itemFields,
|
fields = defaultItemFields + ItemFields.OVERVIEW,
|
||||||
enableResumable = false,
|
enableResumable = false,
|
||||||
)
|
)
|
||||||
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
|
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
|
||||||
@@ -235,7 +233,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
val response = api.userLibraryApi.getLatestMedia(
|
val response = api.userLibraryApi.getLatestMedia(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
parentId = libraryId,
|
parentId = libraryId,
|
||||||
fields = itemFields,
|
fields = defaultItemFields + ItemFields.OVERVIEW,
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.SEASON),
|
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.SEASON),
|
||||||
limit = 10,
|
limit = 10,
|
||||||
)
|
)
|
||||||
@@ -263,7 +261,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
val result = api.tvShowsApi.getSeasons(
|
val result = api.tvShowsApi.getSeasons(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
fields = itemFields,
|
fields = defaultItemFields,
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
)
|
)
|
||||||
Log.d("getSeasons", result.content.toString())
|
Log.d("getSeasons", result.content.toString())
|
||||||
@@ -280,7 +278,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
fields = itemFields,
|
fields = defaultItemFields + ItemFields.OVERVIEW,
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
)
|
)
|
||||||
Log.d("getEpisodesInSeason", result.content.toString())
|
Log.d("getEpisodesInSeason", result.content.toString())
|
||||||
|
|||||||
Reference in New Issue
Block a user