mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
Revert "feat: implement manifest caching for addons"
This reverts commit 018e1e50a8.
This commit is contained in:
parent
db7baf11e3
commit
21793b8ac7
5 changed files with 76 additions and 201 deletions
|
|
@ -19,7 +19,6 @@ import java.util.concurrent.TimeUnit
|
|||
actual object AddonStorage {
|
||||
private const val preferencesName = "nuvio_addons"
|
||||
private const val addonUrlsKey = "installed_manifest_urls"
|
||||
private const val manifestCacheKey = "manifest_cache_payload"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
|
|
@ -42,20 +41,8 @@ actual object AddonStorage {
|
|||
?.putString("${addonUrlsKey}_$profileId", urls.joinToString(separator = "\n"))
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadManifestCachePayload(profileId: Int): String? =
|
||||
preferences?.getString("${manifestCacheKey}_$profileId", null)
|
||||
|
||||
actual fun saveManifestCachePayload(profileId: Int, payload: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString("${manifestCacheKey}_$profileId", payload)
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun addonEpochMs(): Long = System.currentTimeMillis()
|
||||
|
||||
private val addonHttpClient = OkHttpClient.Builder()
|
||||
.dns(IPv4FirstDns())
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
internal data class AddonManifestCachePayload(
|
||||
val entries: List<AddonManifestCacheEntry> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class AddonManifestCacheEntry(
|
||||
val manifestUrl: String,
|
||||
val payload: String,
|
||||
val fetchedAtEpochMs: Long,
|
||||
)
|
||||
|
||||
internal object AddonManifestCacheCodec {
|
||||
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||
|
||||
fun decode(payload: String): List<AddonManifestCacheEntry>? =
|
||||
runCatching {
|
||||
json.decodeFromString(AddonManifestCachePayload.serializer(), payload).entries
|
||||
}.getOrNull()
|
||||
|
||||
fun encode(entries: Collection<AddonManifestCacheEntry>): String =
|
||||
json.encodeToString(
|
||||
AddonManifestCachePayload.serializer(),
|
||||
AddonManifestCachePayload(entries = entries.toList()),
|
||||
)
|
||||
}
|
||||
|
|
@ -3,12 +3,8 @@ package com.nuvio.app.features.addons
|
|||
internal expect object AddonStorage {
|
||||
fun loadInstalledAddonUrls(profileId: Int): List<String>
|
||||
fun saveInstalledAddonUrls(profileId: Int, urls: List<String>)
|
||||
fun loadManifestCachePayload(profileId: Int): String?
|
||||
fun saveManifestCachePayload(profileId: Int, payload: String)
|
||||
}
|
||||
|
||||
internal expect fun addonEpochMs(): Long
|
||||
|
||||
data class RawHttpResponse(
|
||||
val status: Int,
|
||||
val statusText: String,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ object AddonRepository {
|
|||
private var pulledFromServer = false
|
||||
private var currentProfileId: Int = 1
|
||||
private val activeRefreshJobs = mutableMapOf<String, Job>()
|
||||
private var manifestCacheByUrl: Map<String, AddonManifestCacheEntry> = emptyMap()
|
||||
|
||||
fun initialize() {
|
||||
val effectiveProfileId = resolveEffectiveProfileId(ProfileRepository.activeProfileId)
|
||||
|
|
@ -62,12 +61,21 @@ object AddonRepository {
|
|||
|
||||
val storedUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(currentProfileId))
|
||||
log.d { "initialize() — local addon count: ${storedUrls.size}" }
|
||||
if (storedUrls.isEmpty()) {
|
||||
manifestCacheByUrl = emptyMap()
|
||||
return
|
||||
}
|
||||
if (storedUrls.isEmpty()) return
|
||||
|
||||
applyAddonsFromUrls(storedUrls)
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = storedUrls.map { manifestUrl ->
|
||||
existingByUrl[manifestUrl].toPendingAddon(manifestUrl)
|
||||
},
|
||||
)
|
||||
|
||||
storedUrls.forEach { manifestUrl ->
|
||||
val existing = existingByUrl[manifestUrl]
|
||||
if (existing == null || (existing.manifest == null && !existing.isRefreshing)) {
|
||||
refreshAddon(manifestUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onProfileChanged(profileId: Int) {
|
||||
|
|
@ -77,15 +85,11 @@ object AddonRepository {
|
|||
currentProfileId = effectiveProfileId
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
manifestCacheByUrl = emptyMap()
|
||||
_uiState.value = AddonsUiState()
|
||||
}
|
||||
|
||||
fun clearLocalState() {
|
||||
val profileToClear = currentProfileId
|
||||
cancelActiveRefreshes()
|
||||
manifestCacheByUrl = emptyMap()
|
||||
AddonStorage.saveManifestCachePayload(profileToClear, AddonManifestCacheCodec.encode(emptyList()))
|
||||
currentProfileId = 1
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
|
|
@ -145,16 +149,38 @@ object AddonRepository {
|
|||
val localUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(currentProfileId))
|
||||
if (localUrls.isNotEmpty()) {
|
||||
log.w { "pullFromServer() — remote empty while local has ${localUrls.size} addons; preserving local addons" }
|
||||
applyAddonsFromUrls(localUrls)
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = localUrls.map { url ->
|
||||
existingByUrl[url].toPendingAddon(url)
|
||||
},
|
||||
)
|
||||
persist()
|
||||
localUrls.forEach { url ->
|
||||
val existing = existingByUrl[url]
|
||||
if (existing == null || (existing.manifest == null && !existing.isRefreshing)) {
|
||||
refreshAddon(url)
|
||||
}
|
||||
}
|
||||
pulledFromServer = true
|
||||
initialized = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
applyAddonsFromUrls(urls, namesByUrl)
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = urls.map { url ->
|
||||
existingByUrl[url].toPendingAddon(url, namesByUrl[url])
|
||||
},
|
||||
)
|
||||
persist()
|
||||
urls.forEach { url ->
|
||||
val existing = existingByUrl[url]
|
||||
if (existing == null || (existing.manifest == null && !existing.isRefreshing)) {
|
||||
refreshAddon(url)
|
||||
}
|
||||
}
|
||||
pulledFromServer = true
|
||||
initialized = true
|
||||
log.i { "pullFromServer() — applied ${urls.size} addons to state" }
|
||||
|
|
@ -185,19 +211,17 @@ object AddonRepository {
|
|||
return AddAddonResult.Error("That addon is already installed.")
|
||||
}
|
||||
|
||||
val fetched = try {
|
||||
val manifest = try {
|
||||
withContext(Dispatchers.Default) {
|
||||
val payload = httpGetText(manifestUrl)
|
||||
val manifest = AddonManifestParser.parse(
|
||||
AddonManifestParser.parse(
|
||||
manifestUrl = manifestUrl,
|
||||
payload = payload,
|
||||
)
|
||||
payload to manifest
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
return AddAddonResult.Error(error.message ?: "Unable to load manifest")
|
||||
}
|
||||
val (payload, manifest) = fetched
|
||||
|
||||
_uiState.update { current ->
|
||||
current.copy(
|
||||
|
|
@ -209,7 +233,6 @@ object AddonRepository {
|
|||
),
|
||||
)
|
||||
}
|
||||
updateManifestCache(manifestUrl, payload)
|
||||
persist()
|
||||
pushToServer()
|
||||
return AddAddonResult.Success(manifest)
|
||||
|
|
@ -218,16 +241,11 @@ object AddonRepository {
|
|||
fun removeAddon(manifestUrl: String) {
|
||||
if (isUsingPrimaryAddonsFromSecondaryProfile()) return
|
||||
log.i { "removeAddon() — $manifestUrl" }
|
||||
val normalizedUrl = ensureManifestSuffix(manifestUrl)
|
||||
_uiState.update { current ->
|
||||
current.copy(
|
||||
addons = current.addons.filterNot { it.manifestUrl == normalizedUrl },
|
||||
addons = current.addons.filterNot { it.manifestUrl == manifestUrl },
|
||||
)
|
||||
}
|
||||
if (manifestCacheByUrl.containsKey(normalizedUrl)) {
|
||||
manifestCacheByUrl = manifestCacheByUrl - normalizedUrl
|
||||
persistManifestCache()
|
||||
}
|
||||
persist()
|
||||
pushToServer()
|
||||
}
|
||||
|
|
@ -239,34 +257,29 @@ object AddonRepository {
|
|||
}
|
||||
|
||||
fun refreshAddon(manifestUrl: String) {
|
||||
val normalizedUrl = ensureManifestSuffix(manifestUrl)
|
||||
val existingJob = activeRefreshJobs[normalizedUrl]
|
||||
val existingJob = activeRefreshJobs[manifestUrl]
|
||||
if (existingJob?.isActive == true) return
|
||||
|
||||
markRefreshing(normalizedUrl)
|
||||
markRefreshing(manifestUrl)
|
||||
var refreshJob: Job? = null
|
||||
refreshJob = scope.launch {
|
||||
try {
|
||||
val result = runCatching {
|
||||
val payload = httpGetText(normalizedUrl)
|
||||
val manifest = AddonManifestParser.parse(
|
||||
manifestUrl = normalizedUrl,
|
||||
val payload = httpGetText(manifestUrl)
|
||||
AddonManifestParser.parse(
|
||||
manifestUrl = manifestUrl,
|
||||
payload = payload,
|
||||
)
|
||||
payload to manifest
|
||||
}
|
||||
result.onSuccess { (payload, _) ->
|
||||
updateManifestCache(normalizedUrl, payload)
|
||||
}
|
||||
|
||||
_uiState.update { current ->
|
||||
current.copy(
|
||||
addons = current.addons.map { addon ->
|
||||
if (addon.manifestUrl != normalizedUrl) {
|
||||
if (addon.manifestUrl != manifestUrl) {
|
||||
addon
|
||||
} else {
|
||||
result.fold(
|
||||
onSuccess = { (_, manifest) ->
|
||||
onSuccess = { manifest ->
|
||||
addon.copy(
|
||||
manifest = manifest,
|
||||
isRefreshing = false,
|
||||
|
|
@ -276,7 +289,7 @@ object AddonRepository {
|
|||
onFailure = { error ->
|
||||
addon.copy(
|
||||
isRefreshing = false,
|
||||
errorMessage = error.message ?: addon.errorMessage ?: "Unable to load manifest",
|
||||
errorMessage = error.message ?: "Unable to load manifest",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -285,111 +298,12 @@ object AddonRepository {
|
|||
)
|
||||
}
|
||||
} finally {
|
||||
if (activeRefreshJobs[normalizedUrl] === refreshJob) {
|
||||
activeRefreshJobs.remove(normalizedUrl)
|
||||
if (activeRefreshJobs[manifestUrl] === refreshJob) {
|
||||
activeRefreshJobs.remove(manifestUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
activeRefreshJobs[normalizedUrl] = refreshJob
|
||||
}
|
||||
|
||||
private fun applyAddonsFromUrls(
|
||||
urls: List<String>,
|
||||
namesByUrl: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
val normalizedUrls = dedupeManifestUrls(urls)
|
||||
val normalizedUrlSet = normalizedUrls.toSet()
|
||||
val now = addonEpochMs()
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
val loadedCache = loadManifestCacheByUrl()
|
||||
val nextCache = loadedCache.filterKeys { key -> key in normalizedUrlSet }.toMutableMap()
|
||||
|
||||
val addons = normalizedUrls.map { manifestUrl ->
|
||||
val existing = existingByUrl[manifestUrl]
|
||||
val cachedEntry = nextCache[manifestUrl]
|
||||
val cachedManifest = cachedEntry
|
||||
?.takeIf { it.payload.isNotBlank() }
|
||||
?.let { entry ->
|
||||
runCatching {
|
||||
AddonManifestParser.parse(
|
||||
manifestUrl = manifestUrl,
|
||||
payload = entry.payload,
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
if (cachedEntry != null && cachedManifest == null) {
|
||||
nextCache.remove(manifestUrl)
|
||||
}
|
||||
|
||||
val manifest = existing?.manifest ?: cachedManifest
|
||||
val shouldRefresh = when {
|
||||
manifest == null -> true
|
||||
existing?.manifest != null && !existing.isRefreshing -> false
|
||||
cachedEntry == null -> true
|
||||
cachedEntry.fetchedAtEpochMs <= 0L -> true
|
||||
now - cachedEntry.fetchedAtEpochMs >= MANIFEST_CACHE_TTL_MS -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
ManagedAddon(
|
||||
manifestUrl = manifestUrl,
|
||||
manifest = manifest,
|
||||
userSetName = namesByUrl[manifestUrl] ?: existing?.userSetName,
|
||||
isRefreshing = shouldRefresh,
|
||||
errorMessage = if (manifest != null) null else existing?.errorMessage,
|
||||
)
|
||||
}
|
||||
|
||||
manifestCacheByUrl = nextCache.toMap()
|
||||
persistManifestCache()
|
||||
|
||||
_uiState.value = AddonsUiState(addons = addons)
|
||||
addons.filter { it.isRefreshing }.forEach { addon ->
|
||||
refreshAddon(addon.manifestUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadManifestCacheByUrl(): Map<String, AddonManifestCacheEntry> {
|
||||
val payload = AddonStorage.loadManifestCachePayload(currentProfileId).orEmpty()
|
||||
if (payload.isBlank()) {
|
||||
manifestCacheByUrl = emptyMap()
|
||||
return manifestCacheByUrl
|
||||
}
|
||||
|
||||
val decoded = AddonManifestCacheCodec.decode(payload)
|
||||
?.mapNotNull { entry ->
|
||||
val normalizedUrl = ensureManifestSuffix(entry.manifestUrl)
|
||||
if (entry.payload.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
entry.copy(manifestUrl = normalizedUrl)
|
||||
}
|
||||
}
|
||||
?.associateBy(AddonManifestCacheEntry::manifestUrl)
|
||||
.orEmpty()
|
||||
manifestCacheByUrl = decoded
|
||||
return decoded
|
||||
}
|
||||
|
||||
private fun updateManifestCache(manifestUrl: String, payload: String) {
|
||||
if (payload.isBlank()) return
|
||||
val normalizedUrl = ensureManifestSuffix(manifestUrl)
|
||||
manifestCacheByUrl = manifestCacheByUrl + (
|
||||
normalizedUrl to AddonManifestCacheEntry(
|
||||
manifestUrl = normalizedUrl,
|
||||
payload = payload,
|
||||
fetchedAtEpochMs = addonEpochMs(),
|
||||
)
|
||||
)
|
||||
persistManifestCache()
|
||||
}
|
||||
|
||||
private fun persistManifestCache() {
|
||||
AddonStorage.saveManifestCachePayload(
|
||||
profileId = currentProfileId,
|
||||
payload = AddonManifestCacheCodec.encode(manifestCacheByUrl.values),
|
||||
)
|
||||
activeRefreshJobs[manifestUrl] = refreshJob
|
||||
}
|
||||
|
||||
private fun pushToServer() {
|
||||
|
|
@ -462,6 +376,30 @@ object AddonRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ManagedAddon?.toPendingAddon(manifestUrl: String, userSetName: String? = null): ManagedAddon =
|
||||
when {
|
||||
this == null -> ManagedAddon(
|
||||
manifestUrl = manifestUrl,
|
||||
isRefreshing = true,
|
||||
userSetName = userSetName,
|
||||
)
|
||||
manifest != null -> copy(
|
||||
manifestUrl = manifestUrl,
|
||||
isRefreshing = false,
|
||||
userSetName = userSetName ?: this.userSetName,
|
||||
)
|
||||
isRefreshing -> copy(
|
||||
manifestUrl = manifestUrl,
|
||||
userSetName = userSetName ?: this.userSetName,
|
||||
)
|
||||
else -> copy(
|
||||
manifestUrl = manifestUrl,
|
||||
isRefreshing = true,
|
||||
errorMessage = null,
|
||||
userSetName = userSetName ?: this.userSetName,
|
||||
)
|
||||
}
|
||||
|
||||
private fun dedupeManifestUrls(urls: List<String>): List<String> =
|
||||
urls.map(::ensureManifestSuffix).distinct()
|
||||
|
||||
|
|
@ -493,5 +431,3 @@ private fun normalizeManifestUrl(rawUrl: String): String {
|
|||
|
||||
return if (query.isEmpty()) manifestPath else "$manifestPath?$query"
|
||||
}
|
||||
|
||||
private const val MANIFEST_CACHE_TTL_MS = 12L * 60L * 60L * 1000L
|
||||
|
|
|
|||
|
|
@ -15,13 +15,10 @@ import io.ktor.http.ContentType
|
|||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.isSuccess
|
||||
import platform.Foundation.NSDate
|
||||
import platform.Foundation.NSUserDefaults
|
||||
import platform.Foundation.timeIntervalSince1970
|
||||
|
||||
actual object AddonStorage {
|
||||
private const val addonUrlsKey = "installed_manifest_urls"
|
||||
private const val manifestCacheKey = "manifest_cache_payload"
|
||||
|
||||
actual fun loadInstalledAddonUrls(profileId: Int): List<String> =
|
||||
NSUserDefaults.standardUserDefaults
|
||||
|
|
@ -38,18 +35,8 @@ actual object AddonStorage {
|
|||
forKey = "${addonUrlsKey}_$profileId",
|
||||
)
|
||||
}
|
||||
|
||||
actual fun loadManifestCachePayload(profileId: Int): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey("${manifestCacheKey}_$profileId")
|
||||
|
||||
actual fun saveManifestCachePayload(profileId: Int, payload: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = "${manifestCacheKey}_$profileId")
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun addonEpochMs(): Long =
|
||||
(NSDate().timeIntervalSince1970 * 1000.0).toLong()
|
||||
|
||||
private val addonHttpClient = HttpClient(Darwin) {
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 60_000
|
||||
|
|
|
|||
Loading…
Reference in a new issue