refactor(data): remove redundant dedup guards and improve loading robustness

This commit is contained in:
2026-06-22 18:09:50 +00:00
parent 86f44ac9ce
commit 9872f401f8

View File

@@ -1,8 +1,8 @@
package hu.bbara.purefin.data.catalog package hu.bbara.purefin.data.catalog
import hu.bbara.purefin.core.concurrency.SingleFlight
import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.data.UserSessionRepository import hu.bbara.purefin.core.data.UserSessionRepository
import hu.bbara.purefin.core.concurrency.SingleFlight
import hu.bbara.purefin.data.converter.toEpisode import hu.bbara.purefin.data.converter.toEpisode
import hu.bbara.purefin.data.converter.toMovie import hu.bbara.purefin.data.converter.toMovie
import hu.bbara.purefin.data.converter.toSeason import hu.bbara.purefin.data.converter.toSeason
@@ -12,9 +12,6 @@ import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Movie import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Series import hu.bbara.purefin.model.Series
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -36,8 +33,6 @@ class InMemoryLocalMediaRepository @Inject constructor(
private val singleFlight: SingleFlight, private val singleFlight: SingleFlight,
) : LocalMediaRepository { ) : LocalMediaRepository {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val serverUrl = userSessionRepository.serverUrl private val serverUrl = userSessionRepository.serverUrl
private val moviesState = MutableStateFlow<Map<UUID, Movie>>(emptyMap()) private val moviesState = MutableStateFlow<Map<UUID, Movie>>(emptyMap())
@@ -59,7 +54,6 @@ class InMemoryLocalMediaRepository @Inject constructor(
episodesState.map { it[id] }.distinctUntilChanged() episodesState.map { it[id] }.distinctUntilChanged()
override suspend fun loadMovie(id: UUID) = singleFlight.run("LocalMedia:loadMovie:$id") { override suspend fun loadMovie(id: UUID) = singleFlight.run("LocalMedia:loadMovie:$id") {
if (moviesState.value.containsKey(id)) return@run
try { try {
jellyfinApiClient.getItemInfo(id)?.let { item -> jellyfinApiClient.getItemInfo(id)?.let { item ->
if (item.type != BaseItemKind.MOVIE) { if (item.type != BaseItemKind.MOVIE) {
@@ -78,7 +72,6 @@ class InMemoryLocalMediaRepository @Inject constructor(
} }
override suspend fun loadSeries(id: UUID) = singleFlight.run("LocalMedia:loadSeries:$id") { override suspend fun loadSeries(id: UUID) = singleFlight.run("LocalMedia:loadSeries:$id") {
if (seriesState.value.containsKey(id)) return@run
try { try {
jellyfinApiClient.getItemInfo(id)?.let { item -> jellyfinApiClient.getItemInfo(id)?.let { item ->
if (item.type != BaseItemKind.SERIES) { if (item.type != BaseItemKind.SERIES) {
@@ -97,7 +90,6 @@ class InMemoryLocalMediaRepository @Inject constructor(
} }
override suspend fun loadEpisode(id: UUID) = singleFlight.run("LocalMedia:loadEpisode:$id") { override suspend fun loadEpisode(id: UUID) = singleFlight.run("LocalMedia:loadEpisode:$id") {
if (episodesState.value.containsKey(id)) return@run
try { try {
jellyfinApiClient.getItemInfo(id)?.let { item -> jellyfinApiClient.getItemInfo(id)?.let { item ->
if (item.type != BaseItemKind.EPISODE) { if (item.type != BaseItemKind.EPISODE) {
@@ -129,35 +121,35 @@ class InMemoryLocalMediaRepository @Inject constructor(
override suspend fun loadSeasons(seriesId: UUID) = singleFlight.run("LocalMedia:loadSeasons:$seriesId") { override suspend fun loadSeasons(seriesId: UUID) = singleFlight.run("LocalMedia:loadSeasons:$seriesId") {
try { try {
loadSeasonsInternal(seriesId) // Ensure the series is loaded
} catch (error: CancellationException) { var series = seriesState.value[seriesId]
throw error if (series == null) {
} catch (error: Exception) { loadSeries(seriesId)
Timber.tag(TAG).e(error, "Failed to load content for series $seriesId") series = seriesState.first()[seriesId] ?: throw RuntimeException("Series not found")
throw error
} }
}
private suspend fun loadSeasonsInternal(seriesId: UUID) {
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
val updatedSeries = series.copy( val updatedSeries = series.copy(
seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() } seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
) )
seriesState.update { it + (updatedSeries.id to updatedSeries) } seriesState.update { it + (updatedSeries.id to updatedSeries) }
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
Timber.tag(TAG).e(error, "Failed to load seasons for series $seriesId")
throw error
}
} }
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) = override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) =
singleFlight.run("LocalMedia:loadSeasonEpisodes:$seriesId:$seasonId") { singleFlight.run("LocalMedia:loadSeasonEpisodes:$seriesId:$seasonId") {
loadSeasons(seriesId) try {
// Ensure the series and season is loaded
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found") var series = seriesState.value[seriesId]
val season = series.seasons.firstOrNull { it.id == seasonId } ?: return@run if (series == null) {
if (season.episodes.isNotEmpty() || season.episodeCount == 0) { loadSeries(seriesId)
return@run series = seriesState.first()[seriesId] ?: throw RuntimeException("Series not found")
} }
series.seasons.firstOrNull { it.id == seasonId }?.let { loadSeasons(seriesId) }
val serverUrl = userSessionRepository.serverUrl.first() val serverUrl = userSessionRepository.serverUrl.first()
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId) val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId)
@@ -176,6 +168,12 @@ class InMemoryLocalMediaRepository @Inject constructor(
current + (updatedSeries.id to updatedSeries) current + (updatedSeries.id to updatedSeries)
} }
episodesState.update { current -> current + episodes.associateBy { it.id } } episodesState.update { current -> current + episodes.associateBy { it.id } }
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
Timber.tag(TAG).e(error, "Failed to load episodes for season $seasonId")
throw error
}
} }
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) { override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {