diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index 5df53b1c..3bf4715b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.touchlab.kermit.Logger import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.network.NetworkStatusRepository import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding @@ -356,43 +355,6 @@ fun HomeScreen( todayIsoDate = CurrentDateProvider.todayIsoDate(), ) } - LaunchedEffect( - isTraktProgressActive, - traktSettingsUiState.continueWatchingDaysCap, - watchProgressUiState.hasLoadedRemoteProgress, - continueWatchingPreferences.upNextFromFurthestEpisode, - watchProgressUiState.entries, - effectiveWatchProgressEntries, - allNextUpSeedEntries, - recentNextUpSeedEntries, - nextUpSuppressedSeriesIds, - visibleContinueWatchingEntries, - completedSeriesCandidates, - cachedInProgressItems, - cachedNextUpItems, - nextUpItemsBySeries, - processedNextUpContentIds, - effectivNextUpItems, - continueWatchingItems, - ) { - homeCwLog.d { - "build summary source=${if (isTraktProgressActive) "trakt" else "nuvio_sync"} " + - "remoteLoaded=${watchProgressUiState.hasLoadedRemoteProgress} " + - "daysCap=${traktSettingsUiState.continueWatchingDaysCap} " + - "raw=${watchProgressUiState.entries.size} rawSources=${watchProgressUiState.entries.debugSourceCounts()} " + - "effective=${effectiveWatchProgressEntries.size} seedAll=${allNextUpSeedEntries.size} " + - "seedRecent=${recentNextUpSeedEntries.size} seedSuppressed=${nextUpSuppressedSeriesIds.size} " + - "useFurthest=${continueWatchingPreferences.upNextFromFurthestEpisode} " + - "visibleInProgress=${visibleContinueWatchingEntries.size} " + - "completedCandidates=${completedSeriesCandidates.size} cachedInProgress=${cachedInProgressItems.size} " + - "cachedNextUp=${cachedNextUpItems.size} liveNextUp=${nextUpItemsBySeries.size} " + - "processedNextUp=${processedNextUpContentIds.size} " + - "effectiveNextUp=${effectivNextUpItems.size} final=${continueWatchingItems.size} " + - "rawItems=${watchProgressUiState.entries.debugWatchProgressSummary()} " + - "completed=${completedSeriesCandidates.debugCompletedSeriesSummary()} " + - "finalItems=${continueWatchingItems.debugContinueWatchingSummary()}" - } - } val availableManifests = remember(addonsUiState.addons) { addonsUiState.addons.mapNotNull { addon -> addon.manifest } } @@ -430,35 +392,51 @@ fun HomeScreen( LaunchedEffect( completedSeriesCandidates, + cachedNextUpItems, + visibleContinueWatchingEntries, metaProviderKey, continueWatchingPreferences.showUnairedNextUp, ) { if (completedSeriesCandidates.isEmpty()) { - homeCwLog.d { - "next-up resolve skipped: no completed series candidates " + - "entries=${effectiveWatchProgressEntries.size} sources=${effectiveWatchProgressEntries.debugSourceCounts()}" - } nextUpItemsBySeries = emptyMap() processedNextUpContentIds = emptySet() return@LaunchedEffect } - if (metaProviderKey.isEmpty()) { - homeCwLog.d { - "next-up resolve deferred: no meta providers candidates=${completedSeriesCandidates.size} " + - "candidates=${completedSeriesCandidates.debugCompletedSeriesSummary()}" + val cachedResolvedNextUpItems = completedSeriesCandidates.mapNotNull { candidate -> + val cached = cachedNextUpItems[candidate.content.id] ?: return@mapNotNull null + val item = cached.second + if ( + item.nextUpSeedSeasonNumber != candidate.seasonNumber || + item.nextUpSeedEpisodeNumber != candidate.episodeNumber + ) { + return@mapNotNull null } + candidate.content.id to cached + }.toMap() + val candidatesToResolve = completedSeriesCandidates.filter { candidate -> + candidate.content.id !in cachedResolvedNextUpItems + } + if (candidatesToResolve.isEmpty()) { + nextUpItemsBySeries = cachedResolvedNextUpItems + processedNextUpContentIds = completedSeriesCandidates.mapTo(mutableSetOf()) { candidate -> + candidate.content.id + } + saveContinueWatchingSnapshots( + nextUpItemsBySeries = cachedResolvedNextUpItems, + visibleContinueWatchingEntries = visibleContinueWatchingEntries, + todayIsoDate = CurrentDateProvider.todayIsoDate(), + ) + return@LaunchedEffect + } + + if (metaProviderKey.isEmpty()) { return@LaunchedEffect } val todayIsoDate = CurrentDateProvider.todayIsoDate() val semaphore = Semaphore(4) - homeCwLog.d { - "next-up resolve start candidates=${completedSeriesCandidates.size} " + - "showUnaired=${continueWatchingPreferences.showUnairedNextUp} " + - "metaProviders=${metaProviderKey.size} candidates=${completedSeriesCandidates.debugCompletedSeriesSummary()}" - } - val results = completedSeriesCandidates.map { completedEntry -> + val freshResults = candidatesToResolve.map { completedEntry -> async { semaphore.withPermit { val meta = MetaDetailsRepository.fetch( @@ -466,10 +444,6 @@ fun HomeScreen( id = completedEntry.content.id, ) if (meta == null) { - homeCwLog.d { - "next-up meta miss content=${completedEntry.debugSummary()} " + - "type=${completedEntry.content.type} id=${completedEntry.content.id}" - } return@withPermit null } val nextEpisode = meta.nextReleasedEpisodeAfter( @@ -479,84 +453,28 @@ fun HomeScreen( showUnairedNextUp = continueWatchingPreferences.showUnairedNextUp, ) if (nextEpisode == null) { - homeCwLog.d { - "next-up no next episode content=${completedEntry.debugSummary()} " + - "videos=${meta.videos.size}" - } return@withPermit null } val item = completedEntry.toContinueWatchingSeed(meta) .toUpNextContinueWatchingItem(nextEpisode) if (nextUpDismissKey(item.parentMetaId, item.nextUpSeedSeasonNumber, item.nextUpSeedEpisodeNumber) in continueWatchingPreferences.dismissedNextUpKeys) { - homeCwLog.d { "next-up dismissed item=${item.debugSummary()}" } return@withPermit null } - homeCwLog.d { - "next-up built seed=${completedEntry.debugSummary()} item=${item.debugSummary()} " + - "released=${nextEpisode.released}" - } completedEntry.content.id to (completedEntry.markedAtEpochMs to item) } } }.awaitAll().filterNotNull().toMap() + val results = cachedResolvedNextUpItems + freshResults nextUpItemsBySeries = results processedNextUpContentIds = completedSeriesCandidates.mapTo(mutableSetOf()) { candidate -> candidate.content.id } - val nextUpCache = results.mapNotNull { (contentId, pair) -> - val item = pair.second - CachedNextUpItem( - contentId = contentId, - contentType = item.parentMetaType, - name = item.title, - poster = item.poster, - backdrop = item.background, - logo = item.logo, - videoId = item.videoId, - season = item.seasonNumber, - episode = item.episodeNumber, - episodeTitle = item.episodeTitle, - episodeThumbnail = item.episodeThumbnail, - pauseDescription = item.pauseDescription, - released = item.released, - hasAired = item.released?.let { released -> - isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = released) - } ?: true, - lastWatched = pair.first, - sortTimestamp = pair.first, - seedSeason = item.nextUpSeedSeasonNumber, - seedEpisode = item.nextUpSeedEpisodeNumber, - ) - } - val inProgressCache = visibleContinueWatchingEntries.map { entry -> - CachedInProgressItem( - contentId = entry.parentMetaId, - contentType = entry.contentType, - name = entry.title, - poster = entry.poster, - backdrop = entry.background, - logo = entry.logo, - videoId = entry.videoId, - season = entry.seasonNumber, - episode = entry.episodeNumber, - episodeTitle = entry.episodeTitle, - episodeThumbnail = entry.episodeThumbnail, - pauseDescription = entry.pauseDescription, - position = entry.lastPositionMs, - duration = entry.durationMs, - lastWatched = entry.lastUpdatedEpochMs, - progressPercent = entry.progressPercent, - ) - } - ContinueWatchingEnrichmentCache.saveSnapshots( - nextUp = nextUpCache, - inProgress = inProgressCache, + saveContinueWatchingSnapshots( + nextUpItemsBySeries = results, + visibleContinueWatchingEntries = visibleContinueWatchingEntries, + todayIsoDate = todayIsoDate, ) - homeCwLog.d { - "next-up resolve complete results=${results.size} nextUpCache=${nextUpCache.size} " + - "inProgressCache=${inProgressCache.size} items=${results.values.map { it.second }.debugContinueWatchingSummary()}" - } } val hasActiveAddons = addonsUiState.addons.any { it.manifest != null } @@ -784,7 +702,6 @@ fun HomeScreen( private const val HOME_CATALOG_PREVIEW_LIMIT = 18 private const val MILLIS_PER_DAY = 24L * 60L * 60L * 1000L private const val OPTIMISTIC_NEXT_UP_SEED_WINDOW_MS = 3L * 60L * 1000L -private val homeCwLog = Logger.withTag("HomeCW") internal fun filterEntriesForTraktContinueWatchingWindow( entries: List, @@ -1090,6 +1007,62 @@ private data class HomeContinueWatchingCandidate( val isProgressEntry: Boolean, ) +private fun saveContinueWatchingSnapshots( + nextUpItemsBySeries: Map>, + visibleContinueWatchingEntries: List, + todayIsoDate: String, +) { + val nextUpCache = nextUpItemsBySeries.mapNotNull { (contentId, pair) -> + val item = pair.second + CachedNextUpItem( + contentId = contentId, + contentType = item.parentMetaType, + name = item.title, + poster = item.poster, + backdrop = item.background, + logo = item.logo, + videoId = item.videoId, + season = item.seasonNumber, + episode = item.episodeNumber, + episodeTitle = item.episodeTitle, + episodeThumbnail = item.episodeThumbnail, + pauseDescription = item.pauseDescription, + released = item.released, + hasAired = item.released?.let { released -> + isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = released) + } ?: true, + lastWatched = pair.first, + sortTimestamp = pair.first, + seedSeason = item.nextUpSeedSeasonNumber, + seedEpisode = item.nextUpSeedEpisodeNumber, + ) + } + val inProgressCache = visibleContinueWatchingEntries.map { entry -> + CachedInProgressItem( + contentId = entry.parentMetaId, + contentType = entry.contentType, + name = entry.title, + poster = entry.poster, + backdrop = entry.background, + logo = entry.logo, + videoId = entry.videoId, + season = entry.seasonNumber, + episode = entry.episodeNumber, + episodeTitle = entry.episodeTitle, + episodeThumbnail = entry.episodeThumbnail, + pauseDescription = entry.pauseDescription, + position = entry.lastPositionMs, + duration = entry.durationMs, + lastWatched = entry.lastUpdatedEpochMs, + progressPercent = entry.progressPercent, + ) + } + ContinueWatchingEnrichmentCache.saveSnapshots( + nextUp = nextUpCache, + inProgress = inProgressCache, + ) +} + private fun CompletedSeriesCandidate.toContinueWatchingSeed(meta: com.nuvio.app.features.details.MetaDetails) = WatchProgressEntry( contentType = content.type, @@ -1201,83 +1174,3 @@ private fun ContinueWatchingItem.withFallbackMetadata( released = released ?: fallback.released, ) } - -private fun WatchProgressEntry.debugSummary(): String = - buildString { - append(parentMetaType) - append(":") - append(parentMetaId) - if (seasonNumber != null || episodeNumber != null) { - append(" s=") - append(seasonNumber) - append(" e=") - append(episodeNumber) - } - append(" video=") - append(videoId) - append(" pct=") - append(progressPercent) - append(" completed=") - append(isCompleted) - append(" effectiveCompleted=") - append(isEffectivelyCompleted) - append(" src=") - append(source) - append(" last=") - append(lastUpdatedEpochMs) - } - -private fun Collection.debugWatchProgressSummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { it.debugSummary() }.ifBlank { "none" } - -private fun Collection.debugSourceCounts(): String = - groupingBy { it.source } - .eachCount() - .entries - .sortedBy { it.key } - .joinToString(separator = ",") { "${it.key}=${it.value}" } - .ifBlank { "none" } - -private fun CompletedSeriesCandidate.debugSummary(): String = - buildString { - append(content.type) - append(":") - append(content.id) - append(" s=") - append(seasonNumber) - append(" e=") - append(episodeNumber) - append(" marked=") - append(markedAtEpochMs) - } - -private fun Collection.debugCompletedSeriesSummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { it.debugSummary() }.ifBlank { "none" } - -private fun ContinueWatchingItem.debugSummary(): String = - buildString { - append(if (isNextUp) "next_up" else "in_progress") - append(":") - append(parentMetaType) - append(":") - append(parentMetaId) - if (seasonNumber != null || episodeNumber != null) { - append(" s=") - append(seasonNumber) - append(" e=") - append(episodeNumber) - } - append(" video=") - append(videoId) - append(" seed=") - append(nextUpSeedSeasonNumber) - append("x") - append(nextUpSeedEpisodeNumber) - append(" progress=") - append(progressFraction) - append(" resume=") - append(resumePositionMs) - } - -private fun Collection.debugContinueWatchingSummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { it.debugSummary() }.ifBlank { "none" } 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 6d9f99ee..e5361ea2 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 @@ -111,12 +111,10 @@ object TraktProgressRepository { val requestId = nextRefreshRequestId() val headers = TraktAuthRepository.authorizedHeaders() if (headers == null) { - log.d { "refreshNow request=$requestId skipped: missing authorized headers" } _uiState.value = TraktProgressUiState() return } - log.d { "refreshNow request=$requestId start currentEntries=${_uiState.value.entries.size}" } _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) val playbackEntries = runCatching { @@ -140,10 +138,6 @@ object TraktProgressRepository { errorMessage = null, hasLoadedRemoteProgress = false, ) - log.d { - "refreshNow request=$requestId playback applied entries=${playbackEntries.size} " + - "sources=${playbackEntries.debugSourceCounts()} items=${playbackEntries.debugWatchProgressSummary()}" - } if (playbackEntries.isNotEmpty()) { launchHydration(requestId = requestId, entries = playbackEntries) @@ -178,11 +172,6 @@ object TraktProgressRepository { errorMessage = null, hasLoadedRemoteProgress = true, ) - log.d { - "refreshNow request=$requestId completed snapshot applied " + - "completedEntries=${completedEntries.size} merged=${merged.size} " + - "sources=${merged.debugSourceCounts()} items=${merged.debugWatchProgressSummary()}" - } if (merged.isNotEmpty()) { launchHydration(requestId = requestId, entries = merged) @@ -212,10 +201,6 @@ object TraktProgressRepository { isLoading = false, errorMessage = null, ) - log.d { - "hydrate request=$requestId applied hydrated=${hydrated.size} merged=${merged.size} " + - "items=${merged.debugWatchProgressSummary()}" - } } } @@ -228,10 +213,6 @@ object TraktProgressRepository { current[normalizedEntry.videoId] = normalizedEntry } _uiState.value = _uiState.value.copy(entries = current.values.sortedByDescending { it.lastUpdatedEpochMs }) - log.d { - "optimistic progress applied entry=${normalizedEntry.debugSummary()} " + - "entries=${_uiState.value.entries.size}" - } } fun applyOptimisticRemoval(videoId: String) { @@ -239,7 +220,6 @@ object TraktProgressRepository { if (videoId.isBlank()) return val filtered = _uiState.value.entries.filterNot { it.videoId == videoId } _uiState.value = _uiState.value.copy(entries = filtered) - log.d { "optimistic removal videoId=$videoId entries=${filtered.size}" } } fun applyOptimisticRemoval( @@ -260,10 +240,6 @@ object TraktProgressRepository { } } _uiState.value = _uiState.value.copy(entries = filtered) - log.d { - "optimistic removal contentId=$normalizedContentId s=$seasonNumber e=$episodeNumber " + - "entries=${filtered.size}" - } } suspend fun removeProgress( @@ -275,7 +251,6 @@ object TraktProgressRepository { if (normalizedContentId.isBlank()) return val headers = TraktAuthRepository.authorizedHeaders() ?: return - log.d { "removeProgress start contentId=$normalizedContentId s=$seasonNumber e=$episodeNumber" } applyOptimisticRemoval( contentId = normalizedContentId, seasonNumber = seasonNumber, @@ -343,12 +318,10 @@ object TraktProgressRepository { } } - log.d { "removeProgress complete contentId=$normalizedContentId refreshing" } refreshNow() } private suspend fun fetchPlaybackEntries(headers: Map): List = withContext(Dispatchers.Default) { - log.d { "fetchPlaybackEntries start" } val payloads = coroutineScope { val moviesPayload = async { httpGetTextWithHeaders( @@ -371,10 +344,6 @@ object TraktProgressRepository { val moviePlayback = json.decodeFromString>(moviesPayload) val episodePlayback = json.decodeFromString>(episodesPayload) - log.d { - "fetchPlaybackEntries raw movies=${moviePlayback.size} episodes=${episodePlayback.size} " + - "movieItems=${moviePlayback.debugPlaybackSummary()} episodeItems=${episodePlayback.debugPlaybackSummary()}" - } val inProgressMovies = moviePlayback.mapIndexedNotNull { index, item -> mapPlaybackMovie(item = item, fallbackIndex = index) @@ -384,15 +353,10 @@ object TraktProgressRepository { } val merged = mergeNewestByVideoId(inProgressMovies + inProgressEpisodes) - log.d { - "fetchPlaybackEntries mapped movies=${inProgressMovies.size} episodes=${inProgressEpisodes.size} " + - "merged=${merged.size} items=${merged.debugWatchProgressSummary()}" - } merged } private suspend fun fetchHistoryEntries(headers: Map): List = withContext(Dispatchers.Default) { - log.d { "fetchHistoryEntries start limit=$HISTORY_LIMIT" } val payloads = coroutineScope { val historyPayload = async { httpGetTextWithHeaders( @@ -414,10 +378,6 @@ object TraktProgressRepository { val movieHistoryPayload = payloads[1] val episodeHistory = json.decodeFromString>(historyPayload) val movieHistory = json.decodeFromString>(movieHistoryPayload) - log.d { - "fetchHistoryEntries raw episodes=${episodeHistory.size} movies=${movieHistory.size} " + - "episodeItems=${episodeHistory.debugHistoryEpisodeSummary()} movieItems=${movieHistory.debugHistoryMovieSummary()}" - } val completedEpisodes = episodeHistory .mapIndexedNotNull { index, item -> mapHistoryEpisode(item = item, fallbackIndex = index) } @@ -427,10 +387,6 @@ object TraktProgressRepository { .distinctBy { entry -> entry.videoId } val merged = mergeNewestByVideoId(completedEpisodes + completedMovies) - log.d { - "fetchHistoryEntries mapped episodes=${completedEpisodes.size} movies=${completedMovies.size} " + - "merged=${merged.size} items=${merged.debugWatchProgressSummary()}" - } merged } @@ -444,10 +400,6 @@ object TraktProgressRepository { headers = headers, ) val watchedShows = json.decodeFromString>(payload) - log.d { - "fetchWatchedShowSeedEntries raw shows=${watchedShows.size} " + - "items=${watchedShows.debugWatchedShowSummary()}" - } val mapped = watchedShows .mapNotNull { item -> mapWatchedShowSeed( @@ -456,10 +408,6 @@ object TraktProgressRepository { ) } .sortedByDescending { entry -> entry.lastUpdatedEpochMs } - log.d { - "fetchWatchedShowSeedEntries mapped=${mapped.size} useFurthest=$useFurthestEpisode " + - "items=${mapped.debugWatchProgressSummary()}" - } mapped } @@ -914,165 +862,3 @@ private data class TraktEpisode( @SerialName("number") val number: Int? = null, @SerialName("ids") val ids: TraktExternalIds? = null, ) - -private fun WatchProgressEntry.debugSummary(): String = - buildString { - append(parentMetaType) - append(":") - append(parentMetaId) - if (seasonNumber != null || episodeNumber != null) { - append(" s=") - append(seasonNumber) - append(" e=") - append(episodeNumber) - } - append(" video=") - append(videoId) - append(" pct=") - append(progressPercent) - append(" completed=") - append(isCompleted) - append(" effectiveCompleted=") - append(isEffectivelyCompleted) - append(" src=") - append(source) - append(" last=") - append(lastUpdatedEpochMs) - } - -private fun Collection.debugWatchProgressSummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { it.debugSummary() }.ifBlank { "none" } - -private fun Collection.debugSourceCounts(): String = - groupingBy { it.source } - .eachCount() - .entries - .sortedBy { it.key } - .joinToString(separator = ",") { "${it.key}=${it.value}" } - .ifBlank { "none" } - -private fun Collection.debugPlaybackSummary(limit: Int = 8): String = - take(limit).joinToString(separator = " | ") { item -> - val media = item.movie ?: item.show - val episode = item.episode - buildString { - append(media?.title ?: "unknown") - append(" ids=") - append(media?.ids.debugIds()) - if (episode != null) { - append(" ep=") - append(episode.season) - append("x") - append(episode.number) - append(" epIds=") - append(episode.ids.debugIds()) - } - append(" progress=") - append(item.progress) - append(" pausedAt=") - append(item.pausedAt) - append(" playbackId=") - append(item.id) - } - }.ifBlank { "none" } - -private fun Collection.debugHistoryEpisodeSummary(limit: Int = 8): String = - take(limit).joinToString(separator = " | ") { item -> - buildString { - append(item.show?.title ?: "unknown") - append(" ids=") - append(item.show?.ids.debugIds()) - append(" ep=") - append(item.episode?.season) - append("x") - append(item.episode?.number) - append(" epIds=") - append(item.episode?.ids.debugIds()) - append(" watchedAt=") - append(item.watchedAt) - } - }.ifBlank { "none" } - -private fun Collection.debugHistoryMovieSummary(limit: Int = 8): String = - take(limit).joinToString(separator = " | ") { item -> - buildString { - append(item.movie?.title ?: "unknown") - append(" ids=") - append(item.movie?.ids.debugIds()) - append(" watchedAt=") - append(item.watchedAt) - } - }.ifBlank { "none" } - -private fun Collection.debugWatchedShowSummary(limit: Int = 8): String = - take(limit).joinToString(separator = " | ") { item -> - val episodeCount = item.seasons.orEmpty().sumOf { season -> - season.episodes.orEmpty().count { episode -> - (episode.number ?: 0) > 0 && (episode.plays ?: 1) > 0 - } - } - val latest = item.seasons.orEmpty() - .flatMap { season -> - val seasonNumber = season.number - season.episodes.orEmpty().mapNotNull { episode -> - val episodeNumber = episode.number ?: return@mapNotNull null - val watchedAt = episode.lastWatchedAt ?: item.lastWatchedAt - TraktWatchedShowEpisodeSeed( - season = seasonNumber ?: 0, - episode = episodeNumber, - watchedAt = watchedAt - ?.let { value -> - runCatching { TraktPlatformClock.parseIsoDateTimeToEpochMs(value) }.getOrNull() - } - ?: 0L, - ) - } - } - .maxWithOrNull( - compareBy( - { it.watchedAt }, - { it.season }, - { it.episode }, - ), - ) - buildString { - append(item.show?.title ?: "unknown") - append(" ids=") - append(item.show?.ids.debugIds()) - append(" episodes=") - append(episodeCount) - append(" latest=") - append(latest?.season) - append("x") - append(latest?.episode) - append(" lastWatchedAt=") - append(item.lastWatchedAt) - } - }.ifBlank { "none" } - -private fun TraktExternalIds?.debugIds(): String = - if (this == null) { - "none" - } else { - buildString { - imdb?.takeIf { it.isNotBlank() }?.let { - append("imdb:") - append(it) - } - tmdb?.let { - if (isNotEmpty()) append(",") - append("tmdb:") - append(it) - } - trakt?.let { - if (isNotEmpty()) append(",") - append("trakt:") - append(it) - } - slug?.takeIf { it.isNotBlank() }?.let { - if (isNotEmpty()) append(",") - append("slug:") - append(it) - } - }.ifBlank { "none" } - } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/SupabaseProgressSyncAdapter.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/SupabaseProgressSyncAdapter.kt index 80146409..083a3b93 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/SupabaseProgressSyncAdapter.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watching/sync/SupabaseProgressSyncAdapter.kt @@ -1,6 +1,5 @@ package com.nuvio.app.features.watching.sync -import co.touchlab.kermit.Logger import com.nuvio.app.core.network.SupabaseProvider import com.nuvio.app.features.watchprogress.WatchProgressEntry import io.github.jan.supabase.postgrest.postgrest @@ -13,18 +12,16 @@ import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put object SupabaseProgressSyncAdapter : ProgressSyncAdapter { - private val log = Logger.withTag("NuvioSyncProgress") private val json = Json { ignoreUnknownKeys = true encodeDefaults = true } override suspend fun pull(profileId: Int): List { - log.d { "pull start profileId=$profileId" } val params = buildJsonObject { put("p_profile_id", profileId) } val result = SupabaseProvider.client.postgrest.rpc("sync_pull_watch_progress", params) val serverEntries = result.decodeList() - val records = serverEntries.map { entry -> + return serverEntries.map { entry -> ProgressSyncRecord( contentId = entry.contentId, contentType = entry.contentType, @@ -36,21 +33,12 @@ object SupabaseProgressSyncAdapter : ProgressSyncAdapter { lastWatched = entry.lastWatched, ) } - log.d { - "pull returned raw=${serverEntries.size} records=${records.size} " + - "items=${records.debugProgressRecordSummary()}" - } - return records } override suspend fun push( profileId: Int, entries: Collection, ) { - log.d { - "push start profileId=$profileId entries=${entries.size} " + - "items=${entries.debugWatchProgressEntrySummary()}" - } val syncEntries = entries.map { entry -> WatchProgressSyncEntry( contentId = entry.parentMetaId, @@ -69,17 +57,12 @@ object SupabaseProgressSyncAdapter : ProgressSyncAdapter { put("p_entries", json.encodeToJsonElement(syncEntries)) } SupabaseProvider.client.postgrest.rpc("sync_push_watch_progress", params) - log.d { "push complete profileId=$profileId entries=${syncEntries.size}" } } override suspend fun delete( profileId: Int, entries: Collection, ) { - log.d { - "delete start profileId=$profileId entries=${entries.size} " + - "items=${entries.debugWatchProgressEntrySummary()}" - } val progressKeys = entries.map { entry -> if (entry.seasonNumber != null && entry.episodeNumber != null) { "${entry.parentMetaId}_s${entry.seasonNumber}e${entry.episodeNumber}" @@ -92,7 +75,6 @@ object SupabaseProgressSyncAdapter : ProgressSyncAdapter { put("p_keys", json.encodeToJsonElement(progressKeys)) } SupabaseProvider.client.postgrest.rpc("sync_delete_watch_progress", params) - log.d { "delete complete profileId=$profileId keys=${progressKeys.joinToString(limit = 12)}" } } private fun progressKeyForEntry(entry: WatchProgressEntry): String = @@ -115,53 +97,3 @@ private data class WatchProgressSyncEntry( @SerialName("last_watched") val lastWatched: Long = 0, @SerialName("progress_key") val progressKey: String = "", ) - -private fun Collection.debugProgressRecordSummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { record -> - buildString { - append(record.contentType) - append(":") - append(record.contentId) - if (record.season != null || record.episode != null) { - append(" s=") - append(record.season) - append(" e=") - append(record.episode) - } - append(" video=") - append(record.videoId) - append(" pos=") - append(record.position) - append(" dur=") - append(record.duration) - append(" last=") - append(record.lastWatched) - } - }.ifBlank { "none" } - -private fun Collection.debugWatchProgressEntrySummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { entry -> - buildString { - append(entry.parentMetaType) - append(":") - append(entry.parentMetaId) - if (entry.seasonNumber != null || entry.episodeNumber != null) { - append(" s=") - append(entry.seasonNumber) - append(" e=") - append(entry.episodeNumber) - } - append(" video=") - append(entry.videoId) - append(" pos=") - append(entry.lastPositionMs) - append(" dur=") - append(entry.durationMs) - append(" pct=") - append(entry.progressPercent) - append(" completed=") - append(entry.isCompleted) - append(" last=") - append(entry.lastUpdatedEpochMs) - } - }.ifBlank { "none" } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingEnrichmentCache.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingEnrichmentCache.kt index 6152fae8..19d6c046 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingEnrichmentCache.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingEnrichmentCache.kt @@ -60,6 +60,7 @@ internal object ContinueWatchingEnrichmentCache { } private const val storageKey = "cw_enrichment_cache" + private var lastPayloadHash: Int? = null fun getNextUpSnapshot(): List = loadPayload()?.nextUp ?: emptyList() @@ -75,11 +76,17 @@ internal object ContinueWatchingEnrichmentCache { fun saveSnapshots( nextUp: List, inProgress: List, + force: Boolean = false, ) { + val payload = CachedEnrichmentPayload(nextUp = nextUp, inProgress = inProgress) + val payloadHash = payload.hashCode() + if (!force && lastPayloadHash == payloadHash) return + val encoded = runCatching { - json.encodeToString(CachedEnrichmentPayload(nextUp = nextUp, inProgress = inProgress)) + json.encodeToString(payload) }.getOrNull() ?: return ContinueWatchingEnrichmentStorage.savePayload(ProfileScopedKey.of(storageKey), encoded) + lastPayloadHash = payloadHash } private fun loadPayload(): CachedEnrichmentPayload? { @@ -87,6 +94,8 @@ internal object ContinueWatchingEnrichmentCache { ?: return null return runCatching { json.decodeFromString(raw) - }.getOrNull() + }.getOrNull()?.also { payload -> + lastPayloadHash = payload.hashCode() + } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt index 741bfd00..d452d3a8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressRepository.kt @@ -13,7 +13,6 @@ import com.nuvio.app.features.trakt.TraktSettingsRepository import com.nuvio.app.features.trakt.shouldUseTraktProgress as shouldUseTraktProgressSource import com.nuvio.app.features.watching.application.WatchingActions import com.nuvio.app.features.watching.sync.ProgressSyncAdapter -import com.nuvio.app.features.watching.sync.ProgressSyncRecord import com.nuvio.app.features.watching.sync.SupabaseProgressSyncAdapter import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -99,17 +98,7 @@ object WatchProgressRepository { if (authState !is AuthState.Authenticated || authState.isAnonymous) continue if (!hasCompletedInitialNuvioSyncPull || isPullingNuvioSyncFromServer) continue - log.d { - "periodic NuvioSync pull start profileId=${ProfileRepository.activeProfileId} " + - "entries=${entriesByVideoId.size}" - } runCatching { pullFromServer(ProfileRepository.activeProfileId) } - .onSuccess { - log.d { - "periodic NuvioSync pull complete profileId=${ProfileRepository.activeProfileId} " + - "entries=${entriesByVideoId.size}" - } - } .onFailure { error -> if (error is CancellationException) throw error log.w { "Periodic NuvioSync pull failed: ${error.message}" } @@ -171,13 +160,8 @@ object WatchProgressRepository { currentProfileId = profileId val useTraktProgress = shouldUseTraktProgress() - log.d { - "pullFromServer start profileId=$profileId source=${if (useTraktProgress) "trakt" else "nuvio_sync"} " + - "localEntries=${entriesByVideoId.size}" - } if (!useTraktProgress && isPullingNuvioSyncFromServer) { - log.d { "pullFromServer NuvioSync skipped: pull already in flight profileId=$profileId" } return } if (!useTraktProgress) { @@ -192,20 +176,11 @@ object WatchProgressRepository { log.e(e) { "Failed to pull Trakt progress" } } publish() - log.d { - "pullFromServer trakt complete entries=${TraktProgressRepository.uiState.value.entries.size} " + - "sources=${TraktProgressRepository.uiState.value.entries.debugSourceCounts()} " + - "items=${TraktProgressRepository.uiState.value.entries.debugWatchProgressEntrySummary()}" - } return } runCatching { val serverEntries = syncAdapter.pull(profileId = profileId) - log.d { - "pullFromServer NuvioSync returned ${serverEntries.size} records " + - "items=${serverEntries.debugProgressRecordSummary()}" - } val oldLocal = entriesByVideoId.toMap() val newMap = mutableMapOf() @@ -244,10 +219,6 @@ object WatchProgressRepository { hasCompletedInitialNuvioSyncPull = true publish() persist() - log.d { - "pullFromServer NuvioSync applied entries=${entriesByVideoId.size} " + - "items=${entriesByVideoId.values.debugWatchProgressEntrySummary()}" - } resolveRemoteMetadata() }.onFailure { e -> @@ -267,14 +238,8 @@ object WatchProgressRepository { .groupBy { it.parentMetaId to it.contentType } if (needsResolution.isEmpty()) { - log.d { "resolveRemoteMetadata skipped: all entries have artwork" } return } - log.d { - "resolveRemoteMetadata start groups=${needsResolution.size} " + - "entries=${needsResolution.values.sumOf { it.size }} " + - "keys=${needsResolution.keys.joinToString(limit = 12) { (metaId, type) -> "$type:$metaId" }}" - } metadataResolutionJob?.cancel() metadataResolutionJob = syncScope.launch { @@ -291,7 +256,6 @@ object WatchProgressRepository { MetaDetailsRepository.fetch(metaType, metaId) }.getOrNull() if (meta == null) { - log.d { "resolveRemoteMetadata miss type=$metaType id=$metaId entries=${entries.size}" } continue } @@ -316,13 +280,8 @@ object WatchProgressRepository { } publish() - log.d { - "resolveRemoteMetadata applied type=$metaType id=$metaId entries=${entries.size} " + - "metaVideos=${meta.videos.size}" - } } persist() - log.d { "resolveRemoteMetadata complete entries=${entriesByVideoId.size}" } } } @@ -447,10 +406,6 @@ object WatchProgressRepository { isEnded = snapshot.isEnded, ) if (!isCompleted && !shouldStoreWatchProgress(positionMs = positionMs, durationMs = durationMs)) { - log.d { - "upsert skipped below threshold video=${session.videoId} content=${session.parentMetaId} " + - "s=${session.seasonNumber} e=${session.episodeNumber} pos=$positionMs dur=$durationMs ended=${snapshot.isEnded}" - } return } @@ -484,10 +439,6 @@ object WatchProgressRepository { } val useTraktProgress = shouldUseTraktProgress() - log.d { - "upsert progress source=${if (useTraktProgress) "trakt" else "nuvio_sync"} " + - "entry=${entry.debugSummary()} snapshotEnded=${snapshot.isEnded}" - } entriesByVideoId[session.videoId] = entry if (useTraktProgress) { @@ -508,9 +459,7 @@ object WatchProgressRepository { syncScope.launch { runCatching { val profileId = ProfileRepository.activeProfileId - log.d { "pushScrobbleToServer profileId=$profileId entry=${entry.debugSummary()}" } syncAdapter.push(profileId = profileId, entries = listOf(entry)) - log.d { "pushScrobbleToServer complete profileId=$profileId video=${entry.videoId}" } }.onFailure { e -> log.e(e) { "Failed to push watch progress scrobble" } } @@ -523,12 +472,7 @@ object WatchProgressRepository { runCatching { if (entries.isEmpty()) return@runCatching val profileId = ProfileRepository.activeProfileId - log.d { - "pushDeleteToServer profileId=$profileId entries=${entries.size} " + - "items=${entries.debugWatchProgressEntrySummary()}" - } syncAdapter.delete(profileId = profileId, entries = entries) - log.d { "pushDeleteToServer complete profileId=$profileId entries=${entries.size}" } }.onFailure { e -> log.e(e) { "Failed to push watch progress delete" } } @@ -538,11 +482,6 @@ object WatchProgressRepository { private fun publish() { val entries = currentEntries() val sortedEntries = entries.sortedByDescending { it.lastUpdatedEpochMs } - log.d { - "publish source=${if (shouldUseTraktProgress()) "trakt" else "nuvio_sync"} " + - "entries=${sortedEntries.size} cw=${sortedEntries.continueWatchingEntries().size} " + - "sources=${sortedEntries.debugSourceCounts()} items=${sortedEntries.debugWatchProgressEntrySummary()}" - } _uiState.value = WatchProgressUiState( entries = sortedEntries, hasLoadedRemoteProgress = if (shouldUseTraktProgress()) { @@ -575,67 +514,3 @@ object WatchProgressRepository { } } - -private fun ProgressSyncRecord.debugSummary(): String = - buildString { - append(contentType) - append(":") - append(contentId) - if (season != null || episode != null) { - append(" s=") - append(season) - append(" e=") - append(episode) - } - append(" video=") - append(videoId) - append(" pos=") - append(position) - append(" dur=") - append(duration) - append(" last=") - append(lastWatched) - } - -private fun Collection.debugProgressRecordSummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { it.debugSummary() }.ifBlank { "none" } - -private fun WatchProgressEntry.debugSummary(): String = - buildString { - append(parentMetaType) - append(":") - append(parentMetaId) - if (seasonNumber != null || episodeNumber != null) { - append(" s=") - append(seasonNumber) - append(" e=") - append(episodeNumber) - } - append(" video=") - append(videoId) - append(" pos=") - append(lastPositionMs) - append(" dur=") - append(durationMs) - append(" pct=") - append(progressPercent) - append(" completed=") - append(isCompleted) - append(" effectiveCompleted=") - append(isEffectivelyCompleted) - append(" src=") - append(source) - append(" last=") - append(lastUpdatedEpochMs) - } - -private fun Collection.debugWatchProgressEntrySummary(limit: Int = 10): String = - take(limit).joinToString(separator = " | ") { it.debugSummary() }.ifBlank { "none" } - -private fun Collection.debugSourceCounts(): String = - groupingBy { it.source } - .eachCount() - .entries - .sortedBy { it.key } - .joinToString(separator = ",") { "${it.key}=${it.value}" } - .ifBlank { "none" }