mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 23:59:32 +00:00
feat: implement continue watching enrichment storage and caching logic
This commit is contained in:
parent
805ad6c93e
commit
810fe1b253
6 changed files with 254 additions and 17 deletions
|
|
@ -29,6 +29,7 @@ import com.nuvio.app.features.trakt.TraktCommentsStorage
|
|||
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
||||
import com.nuvio.app.features.watched.WatchedStorage
|
||||
import com.nuvio.app.features.streams.StreamLinkCacheStorage
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentStorage
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressStorage
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ class MainActivity : ComponentActivity() {
|
|||
TraktAuthStorage.initialize(applicationContext)
|
||||
TraktCommentsStorage.initialize(applicationContext)
|
||||
ContinueWatchingPreferencesStorage.initialize(applicationContext)
|
||||
ContinueWatchingEnrichmentStorage.initialize(applicationContext)
|
||||
EpisodeReleaseNotificationsStorage.initialize(applicationContext)
|
||||
WatchProgressStorage.initialize(applicationContext)
|
||||
StreamLinkCacheStorage.initialize(applicationContext)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
actual object ContinueWatchingEnrichmentStorage {
|
||||
private const val preferencesName = "nuvio_cw_enrichment"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
fun initialize(context: Context) {
|
||||
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
actual fun loadPayload(key: String): String? =
|
||||
preferences?.getString(key, null)
|
||||
|
||||
actual fun savePayload(key: String, payload: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString(key, payload)
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,9 @@ import com.nuvio.app.features.home.components.HomeSkeletonHero
|
|||
import com.nuvio.app.features.home.components.HomeSkeletonRow
|
||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||
import com.nuvio.app.features.watched.WatchedRepository
|
||||
import com.nuvio.app.features.watchprogress.CachedInProgressItem
|
||||
import com.nuvio.app.features.watchprogress.CachedNextUpItem
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentCache
|
||||
import com.nuvio.app.features.watchprogress.CurrentDateProvider
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||
import com.nuvio.app.features.watchprogress.ContinueWatchingItem
|
||||
|
|
@ -35,6 +38,10 @@ import com.nuvio.app.features.watching.application.WatchingState
|
|||
import com.nuvio.app.features.watching.domain.WatchingContentRef
|
||||
import com.nuvio.app.features.watching.domain.buildPlaybackVideoId
|
||||
import com.nuvio.app.features.watching.domain.isReleasedBy
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
|
|
@ -104,13 +111,26 @@ fun HomeScreen(
|
|||
)
|
||||
}
|
||||
var nextUpItemsBySeries by remember { mutableStateOf<Map<String, Pair<Long, ContinueWatchingItem>>>(emptyMap()) }
|
||||
|
||||
val cachedSnapshots = remember { ContinueWatchingEnrichmentCache.getSnapshots() }
|
||||
val cachedNextUpItems = remember(cachedSnapshots.first) {
|
||||
cachedSnapshots.first.mapNotNull { cached ->
|
||||
val item = cached.toContinueWatchingItem() ?: return@mapNotNull null
|
||||
cached.contentId to (cached.sortTimestamp to item)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
val effectivNextUpItems = remember(nextUpItemsBySeries, cachedNextUpItems) {
|
||||
if (nextUpItemsBySeries.isNotEmpty()) nextUpItemsBySeries else cachedNextUpItems
|
||||
}
|
||||
|
||||
val continueWatchingItems = remember(
|
||||
visibleContinueWatchingEntries,
|
||||
nextUpItemsBySeries,
|
||||
effectivNextUpItems,
|
||||
) {
|
||||
buildHomeContinueWatchingItems(
|
||||
visibleEntries = visibleContinueWatchingEntries,
|
||||
nextUpItemsBySeries = nextUpItemsBySeries,
|
||||
nextUpItemsBySeries = effectivNextUpItems,
|
||||
)
|
||||
}
|
||||
val allManifestsSettled = addonsUiState.addons.isNotEmpty() &&
|
||||
|
|
@ -156,22 +176,71 @@ fun HomeScreen(
|
|||
if (metaProviderKey.isEmpty()) return@LaunchedEffect
|
||||
|
||||
val todayIsoDate = CurrentDateProvider.todayIsoDate()
|
||||
val resolvedItems = mutableMapOf<String, Pair<Long, ContinueWatchingItem>>()
|
||||
completedSeriesCandidates.forEach { completedEntry ->
|
||||
val meta = MetaDetailsRepository.fetch(
|
||||
type = completedEntry.content.type,
|
||||
id = completedEntry.content.id,
|
||||
) ?: return@forEach
|
||||
val nextEpisode = meta.nextReleasedEpisodeAfter(
|
||||
seasonNumber = completedEntry.seasonNumber,
|
||||
episodeNumber = completedEntry.episodeNumber,
|
||||
todayIsoDate = todayIsoDate,
|
||||
showUnairedNextUp = isTraktAuthenticated,
|
||||
) ?: return@forEach
|
||||
resolvedItems[completedEntry.content.id] =
|
||||
completedEntry.markedAtEpochMs to completedEntry.toContinueWatchingSeed(meta).toUpNextContinueWatchingItem(nextEpisode)
|
||||
val semaphore = Semaphore(4)
|
||||
val results = completedSeriesCandidates.map { completedEntry ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
val meta = MetaDetailsRepository.fetch(
|
||||
type = completedEntry.content.type,
|
||||
id = completedEntry.content.id,
|
||||
) ?: return@withPermit null
|
||||
val nextEpisode = meta.nextReleasedEpisodeAfter(
|
||||
seasonNumber = completedEntry.seasonNumber,
|
||||
episodeNumber = completedEntry.episodeNumber,
|
||||
todayIsoDate = todayIsoDate,
|
||||
showUnairedNextUp = isTraktAuthenticated,
|
||||
) ?: return@withPermit null
|
||||
val item = completedEntry.toContinueWatchingSeed(meta)
|
||||
.toUpNextContinueWatchingItem(nextEpisode)
|
||||
completedEntry.content.id to (completedEntry.markedAtEpochMs to item)
|
||||
}
|
||||
}
|
||||
}.awaitAll().filterNotNull().toMap()
|
||||
nextUpItemsBySeries = results
|
||||
|
||||
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,
|
||||
lastWatched = pair.first,
|
||||
sortTimestamp = pair.first,
|
||||
)
|
||||
}
|
||||
nextUpItemsBySeries = resolvedItems
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
val hasActiveAddons = addonsUiState.addons.any { it.manifest != null }
|
||||
|
|
@ -397,3 +466,39 @@ private fun com.nuvio.app.features.details.MetaDetails.nextReleasedEpisodeAfter(
|
|||
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.released)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CachedNextUpItem.toContinueWatchingItem(): ContinueWatchingItem? {
|
||||
val subtitle = buildString {
|
||||
append("Up Next")
|
||||
if (season != null && episode != null) {
|
||||
append(" • S")
|
||||
append(season)
|
||||
append("E")
|
||||
append(episode)
|
||||
}
|
||||
episodeTitle?.takeIf { it.isNotBlank() }?.let {
|
||||
append(" • ")
|
||||
append(it)
|
||||
}
|
||||
}
|
||||
return ContinueWatchingItem(
|
||||
parentMetaId = contentId,
|
||||
parentMetaType = contentType,
|
||||
videoId = videoId,
|
||||
title = name,
|
||||
subtitle = subtitle,
|
||||
imageUrl = episodeThumbnail ?: backdrop ?: poster,
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = backdrop,
|
||||
seasonNumber = season,
|
||||
episodeNumber = episode,
|
||||
episodeTitle = episodeTitle,
|
||||
episodeThumbnail = episodeThumbnail,
|
||||
pauseDescription = pauseDescription,
|
||||
resumePositionMs = 0L,
|
||||
resumeProgressFraction = null,
|
||||
durationMs = 0L,
|
||||
progressFraction = 0f,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class CachedNextUpItem(
|
||||
val contentId: String,
|
||||
val contentType: String,
|
||||
val name: String,
|
||||
val poster: String? = null,
|
||||
val backdrop: String? = null,
|
||||
val logo: String? = null,
|
||||
val videoId: String,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val episodeTitle: String? = null,
|
||||
val episodeThumbnail: String? = null,
|
||||
val pauseDescription: String? = null,
|
||||
val lastWatched: Long,
|
||||
val sortTimestamp: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CachedInProgressItem(
|
||||
val contentId: String,
|
||||
val contentType: String,
|
||||
val name: String,
|
||||
val poster: String? = null,
|
||||
val backdrop: String? = null,
|
||||
val logo: String? = null,
|
||||
val videoId: String,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val episodeTitle: String? = null,
|
||||
val episodeThumbnail: String? = null,
|
||||
val pauseDescription: String? = null,
|
||||
val position: Long,
|
||||
val duration: Long,
|
||||
val lastWatched: Long,
|
||||
val progressPercent: Float? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class CachedEnrichmentPayload(
|
||||
val nextUp: List<CachedNextUpItem> = emptyList(),
|
||||
val inProgress: List<CachedInProgressItem> = emptyList(),
|
||||
)
|
||||
|
||||
internal object ContinueWatchingEnrichmentCache {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private const val storageKey = "cw_enrichment_cache"
|
||||
|
||||
fun getNextUpSnapshot(): List<CachedNextUpItem> =
|
||||
loadPayload()?.nextUp ?: emptyList()
|
||||
|
||||
fun getInProgressSnapshot(): List<CachedInProgressItem> =
|
||||
loadPayload()?.inProgress ?: emptyList()
|
||||
|
||||
fun getSnapshots(): Pair<List<CachedNextUpItem>, List<CachedInProgressItem>> {
|
||||
val payload = loadPayload()
|
||||
return (payload?.nextUp ?: emptyList()) to (payload?.inProgress ?: emptyList())
|
||||
}
|
||||
|
||||
fun saveSnapshots(
|
||||
nextUp: List<CachedNextUpItem>,
|
||||
inProgress: List<CachedInProgressItem>,
|
||||
) {
|
||||
val encoded = runCatching {
|
||||
json.encodeToString(CachedEnrichmentPayload(nextUp = nextUp, inProgress = inProgress))
|
||||
}.getOrNull() ?: return
|
||||
ContinueWatchingEnrichmentStorage.savePayload(ProfileScopedKey.of(storageKey), encoded)
|
||||
}
|
||||
|
||||
private fun loadPayload(): CachedEnrichmentPayload? {
|
||||
val raw = ContinueWatchingEnrichmentStorage.loadPayload(ProfileScopedKey.of(storageKey))
|
||||
?: return null
|
||||
return runCatching {
|
||||
json.decodeFromString<CachedEnrichmentPayload>(raw)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
internal expect object ContinueWatchingEnrichmentStorage {
|
||||
fun loadPayload(key: String): String?
|
||||
fun savePayload(key: String, payload: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
actual object ContinueWatchingEnrichmentStorage {
|
||||
actual fun loadPayload(key: String): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(key)
|
||||
|
||||
actual fun savePayload(key: String, payload: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = key)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue