mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
refactor(data/jellyfin): reload media items on user data change events
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package hu.bbara.purefin.core.data
|
package hu.bbara.purefin.core.data
|
||||||
|
|
||||||
import hu.bbara.purefin.model.Episode
|
import hu.bbara.purefin.model.Episode
|
||||||
|
import hu.bbara.purefin.model.Media
|
||||||
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.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -11,6 +12,14 @@ interface LocalMediaRepository : MediaMetadataUpdater {
|
|||||||
val movies: StateFlow<Map<UUID, Movie>>
|
val movies: StateFlow<Map<UUID, Movie>>
|
||||||
val series: StateFlow<Map<UUID, Series>>
|
val series: StateFlow<Map<UUID, Series>>
|
||||||
val episodes: StateFlow<Map<UUID, Episode>>
|
val episodes: StateFlow<Map<UUID, Episode>>
|
||||||
|
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<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?>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import hu.bbara.purefin.core.data.LocalMediaRepository
|
|||||||
import hu.bbara.purefin.core.data.NetworkMonitor
|
import hu.bbara.purefin.core.data.NetworkMonitor
|
||||||
import hu.bbara.purefin.core.data.UserSessionRepository
|
import hu.bbara.purefin.core.data.UserSessionRepository
|
||||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||||
|
import hu.bbara.purefin.model.Media
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -21,19 +22,7 @@ import timber.log.Timber
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
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
|
@Singleton
|
||||||
class JellyfinWebSocketService @Inject constructor(
|
class JellyfinWebSocketService @Inject constructor(
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
@@ -55,10 +44,6 @@ class JellyfinWebSocketService @Inject constructor(
|
|||||||
// in-flight subscription instead of leaving it running.
|
// in-flight subscription instead of leaving it running.
|
||||||
.collectLatest { active ->
|
.collectLatest { active ->
|
||||||
if (!active) return@collectLatest
|
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) {
|
while (currentCoroutineContext().isActive) {
|
||||||
try {
|
try {
|
||||||
listenForUserDataChanges()
|
listenForUserDataChanges()
|
||||||
@@ -68,7 +53,7 @@ class JellyfinWebSocketService @Inject constructor(
|
|||||||
Timber.tag(TAG).w(error, "UserDataChanged subscription ended, retrying")
|
Timber.tag(TAG).w(error, "UserDataChanged subscription ended, retrying")
|
||||||
}
|
}
|
||||||
if (!currentCoroutineContext().isActive) break
|
if (!currentCoroutineContext().isActive) break
|
||||||
delay(RETRY_DELAY_MS)
|
delay(5000L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,25 +65,26 @@ class JellyfinWebSocketService @Inject constructor(
|
|||||||
.collect { message ->
|
.collect { message ->
|
||||||
val info = message.data ?: return@collect
|
val info = message.data ?: return@collect
|
||||||
for (entry in info.userDataList) {
|
for (entry in info.userDataList) {
|
||||||
val itemId = entry.itemId ?: continue
|
val itemId = entry.itemId
|
||||||
// updateWatchProgressPercent sets both `progress` and a
|
localMediaRepository.getTypeById(itemId).let { media ->
|
||||||
// percent-derived `watched` (>= 90 % rule, see
|
when (media) {
|
||||||
// InMemoryLocalMediaRepository.updateWatchProgressPercent).
|
is Media.MovieMedia -> {
|
||||||
// The follow-up markAsWatched is needed so the server's
|
localMediaRepository.loadMovie(media.id)
|
||||||
// explicit `played` flag wins over the percent-derived
|
}
|
||||||
// boolean when they disagree (e.g. a stale "100 %" entry
|
is Media.EpisodeMedia -> {
|
||||||
// with played = false on an unmark), and so we set
|
localMediaRepository.loadEpisode(media.id)
|
||||||
// `watched = true` for entries that only carry `played`.
|
}
|
||||||
entry.playedPercentage
|
is Media.SeriesMedia -> {
|
||||||
?.takeUnless { it.isNaN() }
|
localMediaRepository.loadSeries(media.id)
|
||||||
?.let { localMediaRepository.updateWatchProgressPercent(itemId, it) }
|
}
|
||||||
localMediaRepository.markAsWatched(itemId, entry.played)
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "JellyfinWebSocket"
|
const val TAG = "JellyfinWebSocket"
|
||||||
const val RETRY_DELAY_MS = 5_000L
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user