feat: refactor SeriesScreen and ViewModel for improved season episode loading and download handling

This commit is contained in:
2026-05-08 18:42:52 +02:00
parent dddf6e4cfa
commit 9d7c166bc8
9 changed files with 131 additions and 88 deletions

View File

@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import java.util.UUID
import javax.inject.Inject
@@ -88,7 +87,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
}
}
val episode = episodesState.value[id] ?: return flowOf(null)
observeSeriesWithContent(seriesId = episode.seriesId)
loadSeasons(seriesId = episode.seriesId)
return episodesState.map { it[id] }
}
@@ -104,18 +103,50 @@ class InMemoryLocalMediaRepository @Inject constructor(
episodesState.update { current -> current + episodes.associateBy { it.id } }
}
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
scope.launch {
try {
ensureSeriesContentLoaded(seriesId)
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
Log.e("InMemoryMediaRepository", "Failed to load content for series $seriesId", error)
throw error
}
override suspend fun loadSeasons(seriesId: UUID) {
try {
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
val updatedSeries = series.copy(
seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
)
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) {
@@ -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 } }
}
}

View File

@@ -44,8 +44,12 @@ class OfflineLocalMediaRepository @Inject constructor(
return episodes.map { it[id] }
}
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
return localDataSource.observeSeriesWithContent(seriesId)
override suspend fun loadSeasons(seriesId: UUID) {
// 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) {

View File

@@ -63,13 +63,11 @@ class JellyfinApiClient @Inject constructor(
private val api = jellyfin.createApi()
private val itemFields =
private val defaultItemFields =
listOf(
ItemFields.CHILD_COUNT,
ItemFields.PARENT_ID,
ItemFields.DATE_LAST_REFRESHED,
ItemFields.OVERVIEW,
ItemFields.SEASON_USER_DATA,
// ItemFields.SEASON_USER_DATA,
)
private suspend fun getUserId(): UUID? = userSessionRepository.userId.first()
@@ -163,7 +161,7 @@ class JellyfinApiClient @Inject constructor(
userId = getUserId(),
enableImages = true,
parentId = libraryId,
fields = itemFields,
fields = defaultItemFields + ItemFields.OVERVIEW,
enableUserData = true,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
recursive = true,
@@ -200,7 +198,7 @@ class JellyfinApiClient @Inject constructor(
val userId = getUserId() ?: return@logApiFailure emptyList()
val getResumeItemsRequest = GetResumeItemsRequest(
userId = userId,
fields = itemFields,
fields = defaultItemFields + ItemFields.OVERVIEW,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
enableUserData = true,
startIndex = 0,
@@ -218,7 +216,7 @@ class JellyfinApiClient @Inject constructor(
}
val getNextUpRequest = GetNextUpRequest(
userId = getUserId(),
fields = itemFields,
fields = defaultItemFields + ItemFields.OVERVIEW,
enableResumable = false,
)
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
@@ -235,7 +233,7 @@ class JellyfinApiClient @Inject constructor(
val response = api.userLibraryApi.getLatestMedia(
userId = getUserId(),
parentId = libraryId,
fields = itemFields,
fields = defaultItemFields + ItemFields.OVERVIEW,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.SEASON),
limit = 10,
)
@@ -263,7 +261,7 @@ class JellyfinApiClient @Inject constructor(
val result = api.tvShowsApi.getSeasons(
userId = getUserId(),
seriesId = seriesId,
fields = itemFields,
fields = defaultItemFields,
enableUserData = true,
)
Log.d("getSeasons", result.content.toString())
@@ -280,7 +278,7 @@ class JellyfinApiClient @Inject constructor(
userId = getUserId(),
seriesId = seriesId,
seasonId = seasonId,
fields = itemFields,
fields = defaultItemFields + ItemFields.OVERVIEW,
enableUserData = true,
)
Log.d("getEpisodesInSeason", result.content.toString())