From 8451fff320a37813c91ab806e1835fed39d4937c Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Thu, 2 Apr 2026 01:36:47 +0530 Subject: [PATCH] feat(trakt): Refactor TraktProgressRepository to improve playback entry fetching and hydration logic --- .../features/trakt/TraktProgressRepository.kt | 123 ++++++++++++------ 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktProgressRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktProgressRepository.kt index dccda5593..7f9ca0ec4 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktProgressRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktProgressRepository.kt @@ -84,48 +84,77 @@ object TraktProgressRepository { _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) - val rawEntries = runCatching { - fetchSnapshotEntries(headers) + val playbackEntries = runCatching { + fetchPlaybackEntries(headers) }.onFailure { error -> if (error is CancellationException) throw error log.w { "Failed to refresh Trakt progress: ${error.message}" } }.getOrNull() - if (rawEntries == null) { + if (playbackEntries == null) { _uiState.value = _uiState.value.copy(isLoading = false, errorMessage = "Failed to load Trakt progress") return } _uiState.value = TraktProgressUiState( - entries = rawEntries, + entries = playbackEntries, isLoading = false, errorMessage = null, ) - if (rawEntries.isNotEmpty()) { - scope.launch { - val hydrated = runCatching { - hydrateEntriesFromAddonMeta(rawEntries) - }.onFailure { error -> - if (error is CancellationException) throw error - log.w { "Failed to hydrate Trakt metadata: ${error.message}" } - }.getOrNull() ?: return@launch + if (playbackEntries.isNotEmpty()) { + launchHydration(requestId = requestId, entries = playbackEntries) + } - if (!isLatestRefreshRequest(requestId)) return@launch + scope.launch { + val historyEntries = runCatching { + fetchHistoryEntries(headers) + }.onFailure { error -> + if (error is CancellationException) throw error + log.w { "Failed to fetch Trakt history snapshot: ${error.message}" } + }.getOrNull() ?: return@launch - val merged = mergeEntriesPreferRichMetadata( - current = _uiState.value.entries, - hydrated = hydrated, - ) - _uiState.value = _uiState.value.copy( - entries = merged.sortedByDescending { it.lastUpdatedEpochMs }, - isLoading = false, - errorMessage = null, - ) + if (!isLatestRefreshRequest(requestId)) return@launch + + val merged = mergeNewestByVideoId(playbackEntries + historyEntries) + _uiState.value = _uiState.value.copy( + entries = merged.sortedByDescending { it.lastUpdatedEpochMs }, + isLoading = false, + errorMessage = null, + ) + + if (merged.isNotEmpty()) { + launchHydration(requestId = requestId, entries = merged) } } } + private fun launchHydration( + requestId: Long, + entries: List, + ) { + scope.launch { + val hydrated = runCatching { + hydrateEntriesFromAddonMeta(entries) + }.onFailure { error -> + if (error is CancellationException) throw error + log.w { "Failed to hydrate Trakt metadata: ${error.message}" } + }.getOrNull() ?: return@launch + + if (!isLatestRefreshRequest(requestId)) return@launch + + val merged = mergeEntriesPreferRichMetadata( + current = _uiState.value.entries, + hydrated = hydrated, + ) + _uiState.value = _uiState.value.copy( + entries = merged.sortedByDescending { it.lastUpdatedEpochMs }, + isLoading = false, + errorMessage = null, + ) + } + } + fun applyOptimisticProgress(entry: WatchProgressEntry) { if (!TraktAuthRepository.isAuthenticated.value) return val current = _uiState.value.entries.associateBy { it.videoId }.toMutableMap() @@ -143,7 +172,7 @@ object TraktProgressRepository { _uiState.value = _uiState.value.copy(entries = filtered) } - private suspend fun fetchSnapshotEntries(headers: Map): List = withContext(Dispatchers.Default) { + private suspend fun fetchPlaybackEntries(headers: Map): List = withContext(Dispatchers.Default) { val payloads = coroutineScope { val moviesPayload = async { httpGetTextWithHeaders( @@ -157,6 +186,28 @@ object TraktProgressRepository { headers = headers, ) } + + awaitAll(moviesPayload, episodesPayload) + } + + val moviesPayload = payloads[0] + val episodesPayload = payloads[1] + + val moviePlayback = json.decodeFromString>(moviesPayload) + val episodePlayback = json.decodeFromString>(episodesPayload) + + val inProgressMovies = moviePlayback.mapIndexedNotNull { index, item -> + mapPlaybackMovie(item = item, fallbackIndex = index) + } + val inProgressEpisodes = episodePlayback.mapIndexedNotNull { index, item -> + mapPlaybackEpisode(item = item, fallbackIndex = index) + } + + mergeNewestByVideoId(inProgressMovies + inProgressEpisodes) + } + + private suspend fun fetchHistoryEntries(headers: Map): List = withContext(Dispatchers.Default) { + val payloads = coroutineScope { val historyPayload = async { httpGetTextWithHeaders( url = "$BASE_URL/sync/history/episodes?limit=$HISTORY_LIMIT", @@ -170,26 +221,14 @@ object TraktProgressRepository { ) } - awaitAll(moviesPayload, episodesPayload, historyPayload, movieHistoryPayload) + awaitAll(historyPayload, movieHistoryPayload) } - val moviesPayload = payloads[0] - val episodesPayload = payloads[1] - val historyPayload = payloads[2] - val movieHistoryPayload = payloads[3] - - val moviePlayback = json.decodeFromString>(moviesPayload) - val episodePlayback = json.decodeFromString>(episodesPayload) + val historyPayload = payloads[0] + val movieHistoryPayload = payloads[1] val episodeHistory = json.decodeFromString>(historyPayload) val movieHistory = json.decodeFromString>(movieHistoryPayload) - val inProgressMovies = moviePlayback.mapIndexedNotNull { index, item -> - mapPlaybackMovie(item = item, fallbackIndex = index) - } - val inProgressEpisodes = episodePlayback.mapIndexedNotNull { index, item -> - mapPlaybackEpisode(item = item, fallbackIndex = index) - } - val completedEpisodes = episodeHistory .mapIndexedNotNull { index, item -> mapHistoryEpisode(item = item, fallbackIndex = index) } .distinctBy { entry -> entry.videoId } @@ -197,15 +236,19 @@ object TraktProgressRepository { .mapIndexedNotNull { index, item -> mapHistoryMovie(item = item, fallbackIndex = index) } .distinctBy { entry -> entry.videoId } + mergeNewestByVideoId(completedEpisodes + completedMovies) + } + + private fun mergeNewestByVideoId(entries: List): List { val mergedByVideoId = linkedMapOf() - (inProgressMovies + inProgressEpisodes + completedEpisodes + completedMovies).forEach { entry -> + entries.forEach { entry -> val existing = mergedByVideoId[entry.videoId] if (existing == null || entry.lastUpdatedEpochMs > existing.lastUpdatedEpochMs) { mergedByVideoId[entry.videoId] = entry } } - mergedByVideoId.values + return mergedByVideoId.values .toList() .sortedByDescending { it.lastUpdatedEpochMs } }