mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 08:39:24 +00:00
Fix mobile continue watching backfill
This commit is contained in:
parent
938f871a91
commit
20620dc400
2 changed files with 163 additions and 2 deletions
|
|
@ -81,6 +81,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.yield
|
||||
import com.nuvio.app.features.trakt.TraktEpisodeMappingService
|
||||
import com.nuvio.app.features.home.components.ContinueWatchingLayout
|
||||
import com.nuvio.app.features.home.components.continueWatchingLandscapeCardHeight
|
||||
|
|
@ -495,7 +496,9 @@ fun HomeScreen(
|
|||
val candidatesToResolve = completedSeriesCandidates.filter { candidate ->
|
||||
candidate.content.id !in cachedResolvedNextUpItems
|
||||
}
|
||||
val resolutionCandidates = candidatesToResolve.take(HomeNextUpInitialResolutionLimit)
|
||||
val resolutionPlan = planHomeNextUpResolutionCandidates(candidatesToResolve)
|
||||
val resolutionCandidates = resolutionPlan.initialCandidates
|
||||
val deferredResolutionCandidates = resolutionPlan.deferredCandidates
|
||||
val seedLastWatchedMap = completedSeriesCandidates.associate { it.content.id to it.markedAtEpochMs }
|
||||
if (candidatesToResolve.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -578,6 +581,59 @@ fun HomeScreen(
|
|||
todayIsoDate = todayIsoDate,
|
||||
seedLastWatchedMap = seedLastWatchedMap,
|
||||
)
|
||||
|
||||
if (deferredResolutionCandidates.isEmpty()) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val deferredCandidateBatches = deferredResolutionCandidates.chunked(NEXT_UP_RESOLUTION_BATCH_SIZE)
|
||||
for (batch in deferredCandidateBatches) {
|
||||
if (cachedResolvedNextUpItems.size + freshResults.size >= HomeContinueWatchingMaxRecentProgressItems) {
|
||||
break
|
||||
}
|
||||
|
||||
val batchResults = batch.map { completedEntry ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
resolveHomeNextUpCandidate(
|
||||
completedEntry = completedEntry,
|
||||
watchProgressEntries = watchProgressUiState.entries,
|
||||
watchedItems = watchedUiState.items,
|
||||
todayIsoDate = todayIsoDate,
|
||||
preferFurthestEpisode = continueWatchingPreferences.upNextFromFurthestEpisode,
|
||||
showUnairedNextUp = continueWatchingPreferences.showUnairedNextUp,
|
||||
dismissedNextUpKeys = continueWatchingPreferences.dismissedNextUpKeys,
|
||||
isTraktProgressActive = isTraktProgressActive,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
batch.forEach { candidate -> processedFreshContentIds += candidate.content.id }
|
||||
|
||||
val resolvedBeforeBatch = freshResults.size
|
||||
batchResults.filterNotNull().forEach { (contentId, item) ->
|
||||
if (cachedResolvedNextUpItems.size + freshResults.size < HomeContinueWatchingMaxRecentProgressItems) {
|
||||
freshResults[contentId] = item
|
||||
}
|
||||
}
|
||||
if (freshResults.size > resolvedBeforeBatch) {
|
||||
val progressiveResults = cachedResolvedNextUpItems + freshResults
|
||||
withContext(Dispatchers.Main) {
|
||||
nextUpItemsBySeries = progressiveResults
|
||||
processedNextUpContentIds = (
|
||||
cachedResolvedNextUpItems.keys +
|
||||
processedFreshContentIds
|
||||
).toSet()
|
||||
}
|
||||
saveContinueWatchingSnapshots(
|
||||
nextUpItemsBySeries = progressiveResults,
|
||||
visibleContinueWatchingEntries = visibleContinueWatchingEntries,
|
||||
todayIsoDate = todayIsoDate,
|
||||
seedLastWatchedMap = seedLastWatchedMap,
|
||||
)
|
||||
}
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -820,6 +876,19 @@ private const val OPTIMISTIC_NEXT_UP_SEED_WINDOW_MS = 3L * 60L * 1000L
|
|||
private const val NEXT_UP_RESOLUTION_CONCURRENCY = 4
|
||||
private const val NEXT_UP_RESOLUTION_BATCH_SIZE = NEXT_UP_RESOLUTION_CONCURRENCY
|
||||
|
||||
internal data class HomeNextUpResolutionPlan(
|
||||
val initialCandidates: List<CompletedSeriesCandidate>,
|
||||
val deferredCandidates: List<CompletedSeriesCandidate>,
|
||||
)
|
||||
|
||||
internal fun planHomeNextUpResolutionCandidates(
|
||||
candidates: List<CompletedSeriesCandidate>,
|
||||
): HomeNextUpResolutionPlan =
|
||||
HomeNextUpResolutionPlan(
|
||||
initialCandidates = candidates.take(HomeNextUpInitialResolutionLimit),
|
||||
deferredCandidates = candidates.drop(HomeNextUpInitialResolutionLimit),
|
||||
)
|
||||
|
||||
internal fun filterEntriesForTraktContinueWatchingWindow(
|
||||
entries: List<WatchProgressEntry>,
|
||||
isTraktProgressActive: Boolean,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
|||
import com.nuvio.app.features.watchprogress.WatchProgressEntry
|
||||
import com.nuvio.app.features.watched.WatchedItem
|
||||
import com.nuvio.app.features.trakt.TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL
|
||||
import com.nuvio.app.features.watching.domain.WatchingContentRef
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
|
@ -23,6 +24,18 @@ class HomeScreenTest {
|
|||
assertEquals(32, HomeNextUpInitialResolutionLimit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home next up resolution keeps candidates beyond initial limit for background resolution`() {
|
||||
val candidates = (1..35).map { index -> completedSeriesCandidate(index) }
|
||||
|
||||
val plan = planHomeNextUpResolutionCandidates(candidates)
|
||||
|
||||
assertEquals(HomeNextUpInitialResolutionLimit, plan.initialCandidates.size)
|
||||
assertEquals(3, plan.deferredCandidates.size)
|
||||
assertEquals("show-1", plan.initialCandidates.first().content.id)
|
||||
assertEquals("show-33", plan.deferredCandidates.first().content.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build home continue watching items removes duplicate video ids`() {
|
||||
val inProgress = progressEntry(
|
||||
|
|
@ -76,6 +89,33 @@ class HomeScreenTest {
|
|||
assertEquals("S1E5 • The Wolf and the Lion", result.single().subtitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build home continue watching items keeps deferred next up items with metadata`() {
|
||||
val nextUpItems = (1..35).associate { index ->
|
||||
val id = "show-$index"
|
||||
val item = continueWatchingItem(
|
||||
videoId = "$id:1:$index",
|
||||
subtitle = "Next Up • S1E$index",
|
||||
imageUrl = "https://example.test/$id.jpg",
|
||||
logo = "https://example.test/$id-logo.png",
|
||||
episodeThumbnail = "https://example.test/$id-thumb.jpg",
|
||||
)
|
||||
|
||||
id to ((10_000L - index) to item)
|
||||
}
|
||||
|
||||
val result = buildHomeContinueWatchingItems(
|
||||
visibleEntries = emptyList(),
|
||||
nextUpItemsBySeries = nextUpItems,
|
||||
)
|
||||
val deferredItem = result.first { item -> item.parentMetaId == "show-33" }
|
||||
|
||||
assertEquals(35, result.size)
|
||||
assertEquals("https://example.test/show-33.jpg", deferredItem.imageUrl)
|
||||
assertEquals("https://example.test/show-33-logo.png", deferredItem.logo)
|
||||
assertEquals("https://example.test/show-33-thumb.jpg", deferredItem.episodeThumbnail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build home continue watching items suppresses next up when series has in progress resume`() {
|
||||
val inProgress = progressEntry(
|
||||
|
|
@ -138,6 +178,45 @@ class HomeScreenTest {
|
|||
assertEquals("GOAT.2026.2160p.UHD.mkv", result.single().title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build home continue watching items preserves cached in progress artwork fallback`() {
|
||||
val progress = progressEntry(
|
||||
videoId = "show:1:4",
|
||||
title = "Show",
|
||||
lastUpdatedEpochMs = 500L,
|
||||
)
|
||||
val cached = ContinueWatchingItem(
|
||||
parentMetaId = "show",
|
||||
parentMetaType = "series",
|
||||
videoId = "show:1:4",
|
||||
title = "Cached Show",
|
||||
subtitle = "S1E4",
|
||||
imageUrl = "https://example.test/cached.jpg",
|
||||
logo = "https://example.test/logo.png",
|
||||
poster = "https://example.test/poster.jpg",
|
||||
background = "https://example.test/backdrop.jpg",
|
||||
seasonNumber = 1,
|
||||
episodeNumber = 4,
|
||||
episodeTitle = "Cached Episode",
|
||||
episodeThumbnail = "https://example.test/thumb.jpg",
|
||||
pauseDescription = "Cached description",
|
||||
isNextUp = false,
|
||||
resumePositionMs = 120_000L,
|
||||
durationMs = 1_000_000L,
|
||||
progressFraction = 0.12f,
|
||||
)
|
||||
|
||||
val result = buildHomeContinueWatchingItems(
|
||||
visibleEntries = listOf(progress),
|
||||
cachedInProgressByVideoId = mapOf(progress.videoId to cached),
|
||||
nextUpItemsBySeries = emptyMap(),
|
||||
)
|
||||
|
||||
assertEquals("https://example.test/cached.jpg", result.single().imageUrl)
|
||||
assertEquals("https://example.test/logo.png", result.single().logo)
|
||||
assertEquals("https://example.test/thumb.jpg", result.single().episodeThumbnail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Trakt continue watching window filters old progress only when Trakt source is active`() {
|
||||
val oldEntry = progressEntry(
|
||||
|
|
@ -310,6 +389,9 @@ class HomeScreenTest {
|
|||
episodeNumber: Int? = 4,
|
||||
seedSeasonNumber: Int? = seasonNumber,
|
||||
seedEpisodeNumber: Int? = episodeNumber,
|
||||
imageUrl: String? = null,
|
||||
logo: String? = null,
|
||||
episodeThumbnail: String? = null,
|
||||
): ContinueWatchingItem =
|
||||
ContinueWatchingItem(
|
||||
parentMetaId = videoId.substringBefore(':'),
|
||||
|
|
@ -317,10 +399,12 @@ class HomeScreenTest {
|
|||
videoId = videoId,
|
||||
title = "Show",
|
||||
subtitle = subtitle,
|
||||
imageUrl = null,
|
||||
imageUrl = imageUrl,
|
||||
logo = logo,
|
||||
seasonNumber = seasonNumber,
|
||||
episodeNumber = episodeNumber,
|
||||
episodeTitle = subtitle.substringAfterLast(" • ", "Episode"),
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
isNextUp = true,
|
||||
nextUpSeedSeasonNumber = seedSeasonNumber,
|
||||
nextUpSeedEpisodeNumber = seedEpisodeNumber,
|
||||
|
|
@ -344,6 +428,14 @@ class HomeScreenTest {
|
|||
markedAtEpochMs = markedAtEpochMs,
|
||||
)
|
||||
|
||||
private fun completedSeriesCandidate(index: Int): CompletedSeriesCandidate =
|
||||
CompletedSeriesCandidate(
|
||||
content = WatchingContentRef(type = "series", id = "show-$index"),
|
||||
seasonNumber = 1,
|
||||
episodeNumber = index,
|
||||
markedAtEpochMs = 10_000L - index,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val MILLIS_PER_DAY = 24L * 60L * 60L * 1000L
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue