refactor(data/jellyfin): reload media items on user data change events

This commit is contained in:
2026-06-20 16:01:22 +00:00
parent 8d5948438e
commit 6e48b35741
2 changed files with 27 additions and 32 deletions

View File

@@ -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
}
}