diff --git a/core/src/main/java/hu/bbara/purefin/core/data/LocalMediaRepository.kt b/core/src/main/java/hu/bbara/purefin/core/data/LocalMediaRepository.kt index 87cc6797..62ee1fcb 100644 --- a/core/src/main/java/hu/bbara/purefin/core/data/LocalMediaRepository.kt +++ b/core/src/main/java/hu/bbara/purefin/core/data/LocalMediaRepository.kt @@ -1,6 +1,7 @@ package hu.bbara.purefin.core.data import hu.bbara.purefin.model.Episode +import hu.bbara.purefin.model.Media import hu.bbara.purefin.model.Movie import hu.bbara.purefin.model.Series import kotlinx.coroutines.flow.Flow @@ -11,6 +12,14 @@ interface LocalMediaRepository : MediaMetadataUpdater { val movies: StateFlow> val series: StateFlow> val episodes: StateFlow> + suspend fun getTypeById(id: UUID): Media? { + return when (val media = movies.value[id] ?: series.value[id] ?: episodes.value[id]) { + is Movie -> Media.MovieMedia(movieId = media.id) + is Series -> Media.SeriesMedia(seriesId = media.id) + is Episode -> Media.EpisodeMedia(seriesId = media.seriesId, episodeId = media.id) + else -> null + } + } suspend fun getMovie(id: UUID): Flow suspend fun getSeries(id: UUID): Flow suspend fun getEpisode(id: UUID): Flow diff --git a/data/src/main/java/hu/bbara/purefin/data/jellyfin/websocket/JellyfinWebSocketService.kt b/data/src/main/java/hu/bbara/purefin/data/jellyfin/websocket/JellyfinWebSocketService.kt index 5b256183..a3719aa6 100644 --- a/data/src/main/java/hu/bbara/purefin/data/jellyfin/websocket/JellyfinWebSocketService.kt +++ b/data/src/main/java/hu/bbara/purefin/data/jellyfin/websocket/JellyfinWebSocketService.kt @@ -4,6 +4,7 @@ import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.data.NetworkMonitor import hu.bbara.purefin.core.data.UserSessionRepository import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient +import hu.bbara.purefin.model.Media import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -21,19 +22,7 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -/** - * Subscribes to the Jellyfin server's [UserDataChangedMessage] WebSocket event - * and propagates the changes to the local media repository so the UI updates - * the affected item in place. - * - * The WebSocket follows the lifecycle of the [JellyfinApiClient]'s [ApiClient][org.jellyfin.sdk.api.client.ApiClient]: - * the SDK automatically reconnects when the server URL, client info, device - * info or access token change (see [org.jellyfin.sdk.api.sockets.SocketApi]). - * - * The subscription is only active while the user is logged in and the device - * is online, matching the gating used by other long-running background work - * such as [hu.bbara.purefin.data.jellyfin.playback.SyncPlaybackPositionsHomeRefreshSideEffect]. - */ + @Singleton class JellyfinWebSocketService @Inject constructor( private val jellyfinApiClient: JellyfinApiClient, @@ -55,10 +44,6 @@ class JellyfinWebSocketService @Inject constructor( // in-flight subscription instead of leaving it running. .collectLatest { active -> if (!active) return@collectLatest - // Re-subscribe in a loop so a transient failure (e.g. the - // first subscription attempt throws because the access - // token is still being applied) does not permanently - // silence the service. while (currentCoroutineContext().isActive) { try { listenForUserDataChanges() @@ -68,7 +53,7 @@ class JellyfinWebSocketService @Inject constructor( Timber.tag(TAG).w(error, "UserDataChanged subscription ended, retrying") } if (!currentCoroutineContext().isActive) break - delay(RETRY_DELAY_MS) + delay(5000L) } } } @@ -80,25 +65,26 @@ class JellyfinWebSocketService @Inject constructor( .collect { message -> val info = message.data ?: return@collect for (entry in info.userDataList) { - val itemId = entry.itemId ?: continue - // updateWatchProgressPercent sets both `progress` and a - // percent-derived `watched` (>= 90 % rule, see - // InMemoryLocalMediaRepository.updateWatchProgressPercent). - // The follow-up markAsWatched is needed so the server's - // explicit `played` flag wins over the percent-derived - // boolean when they disagree (e.g. a stale "100 %" entry - // with played = false on an unmark), and so we set - // `watched = true` for entries that only carry `played`. - entry.playedPercentage - ?.takeUnless { it.isNaN() } - ?.let { localMediaRepository.updateWatchProgressPercent(itemId, it) } - localMediaRepository.markAsWatched(itemId, entry.played) + val itemId = entry.itemId + localMediaRepository.getTypeById(itemId).let { media -> + when (media) { + is Media.MovieMedia -> { + localMediaRepository.loadMovie(media.id) + } + is Media.EpisodeMedia -> { + localMediaRepository.loadEpisode(media.id) + } + is Media.SeriesMedia -> { + localMediaRepository.loadSeries(media.id) + } + else -> {} + } + } } } } private companion object { const val TAG = "JellyfinWebSocket" - const val RETRY_DELAY_MS = 5_000L } }