Try to match CW to the Mobile Implementation
This commit is contained in:
parent
baba142275
commit
d202c14ba8
2 changed files with 75 additions and 12 deletions
|
|
@ -135,8 +135,10 @@ class HomeViewModel @Inject constructor(
|
|||
internal var pendingExternalMetaPrefetchItemId: String? = null
|
||||
internal val prefetchedTmdbIds = Collections.synchronizedSet(mutableSetOf<String>())
|
||||
internal val cwMetaCache = Collections.synchronizedMap(mutableMapOf<String, Meta?>())
|
||||
internal val cwMetaNegativeCacheTimestamps = Collections.synchronizedMap(mutableMapOf<String, Long>())
|
||||
internal val cwTmdbIdCache = Collections.synchronizedMap(mutableMapOf<String, String?>())
|
||||
internal val cwNextUpResolutionCache = Collections.synchronizedMap(mutableMapOf<String, NextUpResolution?>())
|
||||
internal val cwNextUpNegativeCacheTimestamps = Collections.synchronizedMap(mutableMapOf<String, Long>())
|
||||
internal val discoveredOlderNextUpItems = Collections.synchronizedList(mutableListOf<ContinueWatchingItem.NextUp>())
|
||||
internal val fullyWatchedSeriesIds get() = watchedSeriesStateHolder
|
||||
internal var tmdbEnrichFocusJob: Job? = null
|
||||
|
|
|
|||
|
|
@ -35,11 +35,12 @@ import java.time.LocalDateTime
|
|||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
private const val CW_MAX_RECENT_PROGRESS_ITEMS = 150
|
||||
private const val CW_MAX_NEXT_UP_LOOKUPS = 15
|
||||
private const val CW_MAX_RECENT_PROGRESS_ITEMS = 300
|
||||
private const val CW_MAX_NEXT_UP_LOOKUPS = 24
|
||||
private const val CW_MAX_NEXT_UP_CONCURRENCY = 4
|
||||
private const val CW_MAX_ENRICHMENT_CONCURRENCY = 4
|
||||
private const val CW_PROGRESS_DEBOUNCE_MS = 500L
|
||||
|
|
@ -984,12 +985,29 @@ private suspend fun HomeViewModel.findNextUpEpisodeFromMetaSeed(
|
|||
synchronized(cwNextUpResolutionCache) {
|
||||
if (cwNextUpResolutionCache.containsKey(cacheKey)) {
|
||||
val cached = cwNextUpResolutionCache[cacheKey]
|
||||
debug?.recordNextUpCacheHit(
|
||||
progress = progress,
|
||||
resolved = cached != null,
|
||||
showUnairedNextUp = showUnairedNextUp
|
||||
)
|
||||
return cached
|
||||
if (cached != null) {
|
||||
debug?.recordNextUpCacheHit(
|
||||
progress = progress,
|
||||
resolved = true,
|
||||
showUnairedNextUp = showUnairedNextUp
|
||||
)
|
||||
return cached
|
||||
}
|
||||
// Negative cache entry — check TTL
|
||||
val negativeCachedAt = cwNextUpNegativeCacheTimestamps[cacheKey]
|
||||
if (negativeCachedAt != null &&
|
||||
SystemClock.elapsedRealtime() - negativeCachedAt < CW_META_NEGATIVE_CACHE_TTL_MS
|
||||
) {
|
||||
debug?.recordNextUpCacheHit(
|
||||
progress = progress,
|
||||
resolved = false,
|
||||
showUnairedNextUp = showUnairedNextUp
|
||||
)
|
||||
return null
|
||||
}
|
||||
// TTL expired — retry
|
||||
cwNextUpResolutionCache.remove(cacheKey)
|
||||
cwNextUpNegativeCacheTimestamps.remove(cacheKey)
|
||||
}
|
||||
}
|
||||
val contentId = progress.contentId
|
||||
|
|
@ -1008,6 +1026,7 @@ private suspend fun HomeViewModel.findNextUpEpisodeFromMetaSeed(
|
|||
)
|
||||
synchronized(cwNextUpResolutionCache) {
|
||||
cwNextUpResolutionCache[cacheKey] = null
|
||||
cwNextUpNegativeCacheTimestamps[cacheKey] = SystemClock.elapsedRealtime()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -1022,10 +1041,15 @@ private suspend fun HomeViewModel.findNextUpEpisodeFromMetaSeed(
|
|||
logNextUpDecision("drop contentId=$contentId name=${progress.name} reason=no-meta-for-seed")
|
||||
synchronized(cwNextUpResolutionCache) {
|
||||
cwNextUpResolutionCache[cacheKey] = null
|
||||
cwNextUpNegativeCacheTimestamps[cacheKey] = SystemClock.elapsedRealtime()
|
||||
}
|
||||
return null
|
||||
}
|
||||
val nextVideo = resolveNextUpVideoFromMeta(progress, meta, showUnairedNextUp) ?: run {
|
||||
val nextVideo = run {
|
||||
// Fetch watched episodes for this show to skip already-watched episodes (matching mobile)
|
||||
val watchedEpisodes = watchProgressRepository.getWatchedShowEpisodes()[contentId]
|
||||
resolveNextUpVideoFromMeta(progress, meta, showUnairedNextUp, watchedEpisodes)
|
||||
} ?: run {
|
||||
debug?.recordNextUpResult(
|
||||
progress = progress,
|
||||
reason = "no-next-video-after-seed",
|
||||
|
|
@ -1034,6 +1058,7 @@ private suspend fun HomeViewModel.findNextUpEpisodeFromMetaSeed(
|
|||
)
|
||||
synchronized(cwNextUpResolutionCache) {
|
||||
cwNextUpResolutionCache[cacheKey] = null
|
||||
cwNextUpNegativeCacheTimestamps[cacheKey] = SystemClock.elapsedRealtime()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -1078,10 +1103,13 @@ private fun resolveNextUpVideoFromMeta(
|
|||
meta: Meta
|
||||
): Video? = resolveNextUpVideoFromMeta(progress, meta, showUnairedNextUp = true)
|
||||
|
||||
private const val CW_NEXT_UP_NEW_SEASON_UNAIRED_WINDOW_DAYS = 7
|
||||
|
||||
private fun resolveNextUpVideoFromMeta(
|
||||
progress: WatchProgress,
|
||||
meta: Meta,
|
||||
showUnairedNextUp: Boolean
|
||||
showUnairedNextUp: Boolean,
|
||||
watchedEpisodes: Set<Pair<Int, Int>>? = null
|
||||
): Video? {
|
||||
val episodes = meta.videos
|
||||
.filter { video ->
|
||||
|
|
@ -1107,6 +1135,13 @@ private fun resolveNextUpVideoFromMeta(
|
|||
|
||||
val todayLocal = LocalDate.now(ZoneId.systemDefault())
|
||||
val nextVideo = episodes.drop(watchedIndex + 1).firstOrNull { video ->
|
||||
// Skip already-watched episodes (matching mobile's isAlreadyWatched)
|
||||
if (watchedEpisodes != null && video.season != null && video.episode != null) {
|
||||
if ((video.season!! to video.episode!!) in watchedEpisodes) {
|
||||
return@firstOrNull false
|
||||
}
|
||||
}
|
||||
|
||||
val releaseDate = parseEpisodeReleaseDate(video.released)
|
||||
val isSeasonRollover = video.season != seedSeason
|
||||
if (isSeasonRollover) {
|
||||
|
|
@ -1120,6 +1155,13 @@ private fun resolveNextUpVideoFromMeta(
|
|||
if (!releaseDate.isAfter(todayLocal)) {
|
||||
return@firstOrNull true
|
||||
}
|
||||
// Match mobile: show unaired next-season episodes within 7-day window
|
||||
if (showUnairedNextUp) {
|
||||
val daysUntil = java.time.temporal.ChronoUnit.DAYS.between(todayLocal, releaseDate)
|
||||
if (daysUntil <= CW_NEXT_UP_NEW_SEASON_UNAIRED_WINDOW_DAYS) {
|
||||
return@firstOrNull true
|
||||
}
|
||||
}
|
||||
return@firstOrNull false
|
||||
}
|
||||
|
||||
|
|
@ -1143,6 +1185,8 @@ private fun resolveNextUpVideoFromMeta(
|
|||
return nextVideo
|
||||
}
|
||||
|
||||
private const val CW_META_NEGATIVE_CACHE_TTL_MS = 5 * 60_000L
|
||||
|
||||
private suspend fun HomeViewModel.resolveMetaForProgress(
|
||||
progress: WatchProgress,
|
||||
metaCache: MutableMap<String, Meta?>,
|
||||
|
|
@ -1152,8 +1196,20 @@ private suspend fun HomeViewModel.resolveMetaForProgress(
|
|||
val cacheKey = "${progress.contentType}:${progress.contentId}"
|
||||
synchronized(metaCache) {
|
||||
if (metaCache.containsKey(cacheKey)) {
|
||||
debug?.recordMetaCacheHit(progress)
|
||||
return metaCache[cacheKey]
|
||||
val cached = metaCache[cacheKey]
|
||||
if (cached != null) {
|
||||
debug?.recordMetaCacheHit(progress)
|
||||
return cached
|
||||
}
|
||||
val negativeCachedAt = cwMetaNegativeCacheTimestamps[cacheKey]
|
||||
if (negativeCachedAt != null &&
|
||||
SystemClock.elapsedRealtime() - negativeCachedAt < CW_META_NEGATIVE_CACHE_TTL_MS
|
||||
) {
|
||||
debug?.recordMetaCacheHit(progress)
|
||||
return null
|
||||
}
|
||||
metaCache.remove(cacheKey)
|
||||
cwMetaNegativeCacheTimestamps.remove(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1227,6 +1283,11 @@ private suspend fun HomeViewModel.resolveMetaForProgress(
|
|||
|
||||
synchronized(metaCache) {
|
||||
metaCache[cacheKey] = resolved
|
||||
if (resolved == null) {
|
||||
cwMetaNegativeCacheTimestamps[cacheKey] = SystemClock.elapsedRealtime()
|
||||
} else {
|
||||
cwMetaNegativeCacheTimestamps.remove(cacheKey)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue