mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
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:
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package hu.bbara.purefin.core.jellyfin
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
interface JellyfinMediaMetadataUpdater {
|
||||
suspend fun markAsWatched(mediaId: UUID, watched: Boolean)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package hu.bbara.purefin.data.jellyfin
|
||||
|
||||
import hu.bbara.purefin.core.data.LocalMediaRepository
|
||||
import hu.bbara.purefin.core.data.MediaMetadataUpdater
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JellyfinMediaMetadataUpdaterImpl @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient
|
||||
) : JellyfinMediaMetadataUpdater {
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val localMediaRepository: LocalMediaRepository,
|
||||
) : MediaMetadataUpdater {
|
||||
|
||||
override suspend fun markAsWatched(mediaId: UUID, watched: Boolean) {
|
||||
if (watched) {
|
||||
@@ -15,5 +19,22 @@ class JellyfinMediaMetadataUpdaterImpl @Inject constructor(
|
||||
} else {
|
||||
jellyfinApiClient.markAsUnwatched(mediaId)
|
||||
}
|
||||
localMediaRepository.markAsWatched(mediaId, watched)
|
||||
}
|
||||
|
||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||
localMediaRepository.updateWatchProgress(mediaId, positionMs, durationMs)
|
||||
}
|
||||
|
||||
override suspend fun updateWatchProgressPercent(mediaId: UUID, progressPercent: Double) {
|
||||
localMediaRepository.updateWatchProgressPercent(mediaId, progressPercent)
|
||||
}
|
||||
|
||||
override suspend fun updatePlaybackPosition(
|
||||
mediaId: UUID,
|
||||
playbackPositionTicks: Long,
|
||||
runtimeTicks: Long,
|
||||
) {
|
||||
jellyfinApiClient.updatePlaybackPosition(mediaId, playbackPositionTicks, runtimeTicks)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package hu.bbara.purefin.data.jellyfin.playback
|
||||
import hu.bbara.purefin.core.Offline
|
||||
import hu.bbara.purefin.core.Online
|
||||
import hu.bbara.purefin.core.data.LocalMediaRepository
|
||||
import hu.bbara.purefin.core.data.MediaMetadataUpdater
|
||||
import hu.bbara.purefin.core.data.NetworkMonitor
|
||||
import hu.bbara.purefin.core.feature.browse.home.refresh.HomeRefreshSideEffect
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
@@ -19,6 +20,7 @@ import kotlin.math.roundToLong
|
||||
class SyncPlaybackPositionsHomeRefreshSideEffect @Inject constructor(
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val mediaMetadataUpdater: MediaMetadataUpdater,
|
||||
@param:Offline private val offlineRepository: LocalMediaRepository,
|
||||
@param:Online private val onlineRepository: LocalMediaRepository,
|
||||
) : HomeRefreshSideEffect {
|
||||
@@ -49,12 +51,12 @@ class SyncPlaybackPositionsHomeRefreshSideEffect @Inject constructor(
|
||||
// furthest known playback position as the best available source of truth.
|
||||
when {
|
||||
localPlaybackPositionTicks > remotePlaybackPosition.playbackPositionTicks + tickTolerance -> {
|
||||
jellyfinApiClient.updatePlaybackPosition(
|
||||
mediaMetadataUpdater.updatePlaybackPosition(
|
||||
mediaId = localPlaybackPosition.mediaId,
|
||||
playbackPositionTicks = localPlaybackPositionTicks,
|
||||
runtimeTicks = remotePlaybackPosition.runtimeTicks,
|
||||
)
|
||||
onlineRepository.updateWatchProgressPercent(
|
||||
mediaMetadataUpdater.updateWatchProgressPercent(
|
||||
mediaId = localPlaybackPosition.mediaId,
|
||||
progressPercent = localPlaybackPositionTicks.toProgressPercent(
|
||||
runtimeTicks = remotePlaybackPosition.runtimeTicks,
|
||||
@@ -65,11 +67,7 @@ class SyncPlaybackPositionsHomeRefreshSideEffect @Inject constructor(
|
||||
val remoteProgressPercent = remotePlaybackPosition.playbackPositionTicks.toProgressPercent(
|
||||
runtimeTicks = remotePlaybackPosition.runtimeTicks,
|
||||
)
|
||||
offlineRepository.updateWatchProgressPercent(
|
||||
mediaId = localPlaybackPosition.mediaId,
|
||||
progressPercent = remoteProgressPercent,
|
||||
)
|
||||
onlineRepository.updateWatchProgressPercent(
|
||||
mediaMetadataUpdater.updateWatchProgressPercent(
|
||||
mediaId = localPlaybackPosition.mediaId,
|
||||
progressPercent = remoteProgressPercent,
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import hu.bbara.purefin.data.catalog.InMemoryLocalMediaRepository
|
||||
import hu.bbara.purefin.data.catalog.OfflineLocalMediaRepository
|
||||
import hu.bbara.purefin.data.jellyfin.JellyfinMediaMetadataUpdaterImpl
|
||||
import hu.bbara.purefin.data.jellyfin.SearchManagerImpl
|
||||
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
|
||||
import hu.bbara.purefin.core.data.MediaMetadataUpdater
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -32,7 +32,7 @@ abstract class MediaRepositoryModule {
|
||||
abstract fun bindHomeRepository(impl: InMemoryAppContentRepository): HomeRepository
|
||||
|
||||
@Binds
|
||||
abstract fun bindJellyfinMediaMetadataUpdater(impl: JellyfinMediaMetadataUpdaterImpl): JellyfinMediaMetadataUpdater
|
||||
abstract fun bindMediaMetadataUpdater(impl: JellyfinMediaMetadataUpdaterImpl): MediaMetadataUpdater
|
||||
|
||||
@Binds
|
||||
abstract fun bindSearchManager(impl: SearchManagerImpl): SearchManager
|
||||
|
||||
Reference in New Issue
Block a user