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