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 ff3d8c8f8..35eab018f 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 @@ -43,6 +43,9 @@ private const val TRAKT_COMPLETION_PERCENT_THRESHOLD = 90f private const val HISTORY_PAGE_LIMIT = 1000 private const val HISTORY_MAX_PAGES = 5 private const val HISTORY_MAX_PAGES_ALL = 20 +private const val WATCHED_PAGE_LIMIT = 250 +private const val WATCHED_MAX_PAGES = 1_000 +private const val WATCHED_SHOWS_EXTENDED = "progress" private const val MAX_RECENT_EPISODE_HISTORY_ENTRIES = 300 private const val METADATA_FETCH_TIMEOUT_MS = 3_500L private const val METADATA_FETCH_CONCURRENCY = 5 @@ -688,11 +691,7 @@ object TraktProgressRepository { ): List = withContext(Dispatchers.Default) { ContinueWatchingPreferencesRepository.ensureLoaded() val useFurthestEpisode = ContinueWatchingPreferencesRepository.uiState.value.upNextFromFurthestEpisode - val payload = httpGetTextWithHeaders( - url = "$BASE_URL/sync/watched/shows", - headers = headers, - ) - val watchedShows = json.decodeFromString>(payload) + val watchedShows = fetchWatchedShowPages(headers) updateWatchedShowCaches(watchedShows) val mapped = fixAmbiguousWatchedShowSeeds( watchedShows.mapNotNull { item -> @@ -706,6 +705,34 @@ object TraktProgressRepository { mapped } + private suspend fun fetchWatchedShowPages( + headers: Map, + ): List { + val items = mutableListOf() + var page = 1 + while (page <= WATCHED_MAX_PAGES) { + val response = httpRequestRaw( + method = "GET", + url = "$BASE_URL/sync/watched/shows?page=$page&limit=$WATCHED_PAGE_LIMIT&extended=$WATCHED_SHOWS_EXTENDED", + headers = headers, + body = "", + ) + if (response.status !in 200..299) { + error("Trakt watched shows request failed: ${response.status}") + } + val pageItems = json.decodeFromString>(response.body) + if (pageItems.isEmpty()) break + items.addAll(pageItems) + val pageCount = response.headerInt("x-pagination-page-count") + if (pageCount != null && page >= pageCount) break + page += 1 + } + if (page > WATCHED_MAX_PAGES) { + error("Trakt watched shows exceeded max pages") + } + return items + } + private fun updateWatchedShowCaches(items: List) { val siblingsMap = mutableMapOf>() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/TraktWatchedSyncAdapter.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/TraktWatchedSyncAdapter.kt index f3c825916..b2490f75b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/TraktWatchedSyncAdapter.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/TraktWatchedSyncAdapter.kt @@ -1,7 +1,7 @@ package com.nuvio.app.features.watching.sync import co.touchlab.kermit.Logger -import com.nuvio.app.features.addons.httpGetTextWithHeaders +import com.nuvio.app.features.addons.RawHttpResponse import com.nuvio.app.features.addons.httpRequestRaw import com.nuvio.app.features.tmdb.TmdbService import com.nuvio.app.features.trakt.TraktAuthRepository @@ -18,6 +18,9 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json private const val BASE_URL = "https://api.trakt.tv" +private const val WATCHED_PAGE_LIMIT = 250 +private const val WATCHED_MAX_PAGES = 1_000 +private const val WATCHED_SHOWS_EXTENDED = "progress" object TraktWatchedSyncAdapter : WatchedSyncAdapter { @@ -35,30 +38,16 @@ object TraktWatchedSyncAdapter : WatchedSyncAdapter { ): List { val headers = TraktAuthRepository.authorizedHeaders() ?: return emptyList() - val (moviesPayload, showsPayload) = coroutineScope { + val (movieItems, showItems) = coroutineScope { val movies = async { - httpGetTextWithHeaders( - url = "$BASE_URL/sync/watched/movies", - headers = headers, - ) + fetchWatchedMoviePages(headers) } val shows = async { - httpGetTextWithHeaders( - url = "$BASE_URL/sync/watched/shows", - headers = headers, - ) + fetchWatchedShowPages(headers) } movies.await() to shows.await() } - val movieItems = runCatching { - json.decodeFromString>(moviesPayload) - }.getOrDefault(emptyList()) - - val showItems = runCatching { - json.decodeFromString>(showsPayload) - }.getOrDefault(emptyList()) - val result = mutableListOf() movieItems.forEach { item -> @@ -122,6 +111,64 @@ object TraktWatchedSyncAdapter : WatchedSyncAdapter { return remappedResult } + private suspend fun fetchWatchedMoviePages(headers: Map): List { + val items = mutableListOf() + var page = 1 + while (page <= WATCHED_MAX_PAGES) { + val response = httpRequestRaw( + method = "GET", + url = "$BASE_URL/sync/watched/movies?page=$page&limit=$WATCHED_PAGE_LIMIT", + headers = headers, + body = "", + ) + if (response.status !in 200..299) { + error("Trakt watched movies request failed: ${response.status}") + } + val pageItems = json.decodeFromString>(response.body) + if (pageItems.isEmpty()) break + items.addAll(pageItems) + val pageCount = response.headerInt("x-pagination-page-count") + if (pageCount != null && page >= pageCount) break + page += 1 + } + if (page > WATCHED_MAX_PAGES) { + error("Trakt watched movies exceeded max pages") + } + return items + } + + private suspend fun fetchWatchedShowPages(headers: Map): List { + val items = mutableListOf() + var page = 1 + while (page <= WATCHED_MAX_PAGES) { + val response = httpRequestRaw( + method = "GET", + url = "$BASE_URL/sync/watched/shows?page=$page&limit=$WATCHED_PAGE_LIMIT&extended=$WATCHED_SHOWS_EXTENDED", + headers = headers, + body = "", + ) + if (response.status !in 200..299) { + error("Trakt watched shows request failed: ${response.status}") + } + val pageItems = json.decodeFromString>(response.body) + if (pageItems.isEmpty()) break + items.addAll(pageItems) + val pageCount = response.headerInt("x-pagination-page-count") + if (pageCount != null && page >= pageCount) break + page += 1 + } + if (page > WATCHED_MAX_PAGES) { + error("Trakt watched shows exceeded max pages") + } + return items + } + + private fun RawHttpResponse.headerInt(name: String): Int? = + headers[name.lowercase()] + ?.substringBefore(",") + ?.trim() + ?.toIntOrNull() + // ── push (add to history) ─────────────────────────────────────────── override suspend fun push( profileId: Int,