refactor(media): consolidate metadata update functions into MediaMetadataUpdater

Replace the disparate JellyfinMediaMetadataUpdater, LocalMediaUpdater,
and direct API/repository calls for watched status and progress updates
with a single unified MediaMetadataUpdater interface and implementation.

The new implementation coordinates both local cache (in-memory/Room)
and remote server updates, so callers have a single point of entry
for all metadata mutations. markAsWatched now updates both server
and local state, fixing a gap where the UI would not reflect
watched changes until a full refresh.
This commit is contained in:
2026-06-12 18:08:32 +00:00
parent def92ad111
commit e78a3700a6
11 changed files with 90 additions and 39 deletions

View File

@@ -0,0 +1,39 @@
package hu.bbara.purefin.core.data
import java.util.UUID
/**
* Unified interface for updating media metadata (watched status, watch progress,
* and server-side playback position).
*
* Implementations coordinate both local cache updates and remote server calls
* so that callers have a single point of entry for all metadata mutations.
*/
interface MediaMetadataUpdater {
/**
* Mark the given media item as watched or unwatched.
* Updates both the local cache and the remote server.
*/
suspend fun markAsWatched(mediaId: UUID, watched: Boolean)
/**
* Compute progress from absolute position/duration and persist it
* to the local cache (in-memory and/or Room).
*/
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long)
/**
* Persist a raw progress percentage (0.0 100.0) to the local cache.
*/
suspend fun updateWatchProgressPercent(mediaId: UUID, progressPercent: Double)
/**
* Update the server-side playback position (in ticks) and optionally
* mark the item as played when past the threshold (≥ 80 %).
*/
suspend fun updatePlaybackPosition(
mediaId: UUID,
playbackPositionTicks: Long,
runtimeTicks: Long,
)
}

View File

@@ -8,7 +8,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.core.feature.browse.home.refresh.HomeRefreshCoordinator
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
import hu.bbara.purefin.core.data.MediaMetadataUpdater
import hu.bbara.purefin.core.settings.SettingsRepository
import hu.bbara.purefin.core.model.EpisodeUiModel
import hu.bbara.purefin.core.model.LibraryUiModel
@@ -39,7 +39,7 @@ class AppViewModel @Inject constructor(
private val homeRepository: HomeRepository,
private val localMediaRepository: LocalMediaRepository,
private val userSessionRepository: UserSessionRepository,
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
private val mediaMetadataUpdater: MediaMetadataUpdater,
private val navigationManager: NavigationManager,
private val homeRefreshCoordinator: HomeRefreshCoordinator,
private val settingsRepository: SettingsRepository,
@@ -185,7 +185,7 @@ class AppViewModel @Inject constructor(
fun markAsWatched(mediaUiModel: MediaUiModel, watched: Boolean) {
viewModelScope.launch {
jellyfinMediaMetadataUpdater.markAsWatched(mediaUiModel.id, watched)
mediaMetadataUpdater.markAsWatched(mediaUiModel.id, watched)
}
}

View File

@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.data.HomeRepository
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
import hu.bbara.purefin.core.data.MediaMetadataUpdater
import hu.bbara.purefin.core.model.MediaUiModel
import hu.bbara.purefin.core.model.MovieUiModel
import hu.bbara.purefin.core.model.SeriesUiModel
@@ -26,7 +26,7 @@ import javax.inject.Inject
class LibraryViewModel @Inject constructor(
private val homeRepository: HomeRepository,
private val navigationManager: NavigationManager,
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
private val mediaMetadataUpdater: MediaMetadataUpdater,
) : ViewModel() {
private val selectedLibrary = MutableStateFlow<UUID?>(null)
@@ -73,7 +73,7 @@ class LibraryViewModel @Inject constructor(
fun markAsWatched(mediaUiModel: MediaUiModel, watched: Boolean) {
viewModelScope.launch {
jellyfinMediaMetadataUpdater.markAsWatched(mediaUiModel.id, watched)
mediaMetadataUpdater.markAsWatched(mediaUiModel.id, watched)
}
}

View File

@@ -7,7 +7,7 @@ import hu.bbara.purefin.core.Offline
import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.download.MediaDownloadController
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
import hu.bbara.purefin.core.data.MediaMetadataUpdater
import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route
@@ -31,7 +31,7 @@ class EpisodeScreenViewModel @Inject constructor(
@param:Offline private val offlineMediaCatalogReader: LocalMediaRepository,
private val navigationManager: NavigationManager,
private val mediaDownloadManager: MediaDownloadController,
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
private val mediaMetadataUpdater: MediaMetadataUpdater,
): ViewModel() {
private val _episode = MutableStateFlow<EpisodeDto?>(null)
@@ -86,7 +86,7 @@ class EpisodeScreenViewModel @Inject constructor(
fun markAsWatched(watched: Boolean) {
val episodeId = _episode.value?.id ?: return
viewModelScope.launch {
jellyfinMediaMetadataUpdater.markAsWatched(episodeId, watched)
mediaMetadataUpdater.markAsWatched(episodeId, watched)
}
}

View File

@@ -7,7 +7,7 @@ import hu.bbara.purefin.core.Offline
import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.download.MediaDownloadController
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
import hu.bbara.purefin.core.data.MediaMetadataUpdater
import hu.bbara.purefin.core.navigation.MovieDto
import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route
@@ -29,7 +29,7 @@ class MovieScreenViewModel @Inject constructor(
@param:Offline private val offlineMediaCatalogReader: LocalMediaRepository,
private val navigationManager: NavigationManager,
private val mediaDownloadManager: MediaDownloadController,
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
private val mediaMetadataUpdater: MediaMetadataUpdater,
): ViewModel() {
private val _movie = MutableStateFlow<MovieDto?>(null)
@@ -73,7 +73,7 @@ class MovieScreenViewModel @Inject constructor(
fun markAsWatched(watched: Boolean) {
val movieId = movie.value?.id ?: return
viewModelScope.launch {
jellyfinMediaMetadataUpdater.markAsWatched(movieId, watched)
mediaMetadataUpdater.markAsWatched(movieId, watched)
}
}

View File

@@ -7,7 +7,7 @@ import hu.bbara.purefin.core.Offline
import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.download.MediaDownloadController
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
import hu.bbara.purefin.core.data.MediaMetadataUpdater
import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route
@@ -34,7 +34,7 @@ class SeriesViewModel @Inject constructor(
@param:Offline private val offlineMediaCatalogReader: LocalMediaRepository,
private val navigationManager: NavigationManager,
private val mediaDownloadManager: MediaDownloadController,
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
private val mediaMetadataUpdater: MediaMetadataUpdater,
) : ViewModel() {
private val _series = MutableStateFlow<SeriesDto?>(null)
@@ -188,7 +188,7 @@ class SeriesViewModel @Inject constructor(
fun markAsWatched(watched: Boolean) {
val seriesId = _series.value?.id ?: return
viewModelScope.launch {
jellyfinMediaMetadataUpdater.markAsWatched(seriesId, watched)
mediaMetadataUpdater.markAsWatched(seriesId, watched)
}
}

View File

@@ -1,7 +0,0 @@
package hu.bbara.purefin.core.jellyfin
import java.util.UUID
interface JellyfinMediaMetadataUpdater {
suspend fun markAsWatched(mediaId: UUID, watched: Boolean)
}

View File

@@ -1,7 +1,7 @@
package hu.bbara.purefin.core.player.manager
import dagger.hilt.android.scopes.ViewModelScoped
import hu.bbara.purefin.core.data.LocalMediaUpdater
import hu.bbara.purefin.core.data.MediaMetadataUpdater
import hu.bbara.purefin.core.data.PlaybackProgressReporter
import hu.bbara.purefin.core.data.PlaybackReportContext
import hu.bbara.purefin.core.player.model.MetadataState
@@ -24,7 +24,7 @@ import javax.inject.Inject
@ViewModelScoped
class ProgressManager @Inject constructor(
private val playbackProgressReporter: PlaybackProgressReporter,
private val localMediaUpdater: LocalMediaUpdater,
private val mediaMetadataUpdater: MediaMetadataUpdater,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private var progressJob: Job? = null
@@ -80,7 +80,7 @@ class ProgressManager @Inject constructor(
report(itemId, lastPositionMs, reportContext = activePlaybackReportContext, isStop = true)
scope.launch(Dispatchers.IO) {
try {
localMediaUpdater.updateWatchProgress(itemId, lastPositionMs, lastDurationMs)
mediaMetadataUpdater.updateWatchProgress(itemId, lastPositionMs, lastDurationMs)
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Local cache update failed")
}
@@ -127,7 +127,7 @@ class ProgressManager @Inject constructor(
activePlaybackReportContext?.let { reportContext ->
playbackProgressReporter.reportPlaybackStopped(itemId, ticks, reportContext)
}
localMediaUpdater.updateWatchProgress(itemId, posMs, durMs)
mediaMetadataUpdater.updateWatchProgress(itemId, posMs, durMs)
Timber.tag(TAG).d("Stop: $itemId at ${posMs}ms")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Report failed")