mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-06 19:38:58 +00:00
feat: Enhance synchronization process in SyncManager and AddonRepository
- Improved logging for pull operations in SyncManager, detailing the authentication state and pull sequence. - Refactored AddonRepository to streamline addon retrieval from the server, including migration of local addons if the server returns no results. - Added timeout handling for waiting on addon manifests in WatchProgressRepository to prevent indefinite blocking.
This commit is contained in:
parent
d9fd0e49d5
commit
a0559012b9
3 changed files with 105 additions and 28 deletions
|
|
@ -23,8 +23,14 @@ object SyncManager {
|
|||
if (authState.isAnonymous) return
|
||||
|
||||
scope.launch {
|
||||
log.i { "Starting pull-all for profile $profileId" }
|
||||
log.i { "pullAllForProfile($profileId) — auth=${(authState as AuthState.Authenticated).isAnonymous}" }
|
||||
|
||||
log.i { "pullAllForProfile — pulling addons first (await)..." }
|
||||
runCatching { AddonRepository.pullFromServer(profileId) }
|
||||
.onSuccess { log.i { "pullAllForProfile — addons pull completed" } }
|
||||
.onFailure { log.e(it) { "Addon pull failed" } }
|
||||
|
||||
log.i { "pullAllForProfile — launching remaining pulls in parallel" }
|
||||
launch {
|
||||
runCatching { LibraryRepository.pullFromServer(profileId) }
|
||||
.onFailure { log.e(it) { "Library pull failed" } }
|
||||
|
|
@ -33,10 +39,6 @@ object SyncManager {
|
|||
runCatching { WatchProgressRepository.pullFromServer(profileId) }
|
||||
.onFailure { log.e(it) { "WatchProgress pull failed" } }
|
||||
}
|
||||
launch {
|
||||
runCatching { AddonRepository.pullFromServer(profileId) }
|
||||
.onFailure { log.e(it) { "Addon pull failed" } }
|
||||
}
|
||||
launch {
|
||||
runCatching { WatchedRepository.pullFromServer(profileId) }
|
||||
.onFailure { log.e(it) { "Watched pull failed" } }
|
||||
|
|
@ -46,7 +48,7 @@ object SyncManager {
|
|||
.onFailure { log.e(it) { "ProfileSettings pull failed" } }
|
||||
}
|
||||
|
||||
log.i { "Pull-all launched for profile $profileId" }
|
||||
log.i { "pullAllForProfile($profileId) — all pulls launched" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import co.touchlab.kermit.Logger
|
|||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
import io.github.jan.supabase.postgrest.query.Order
|
||||
import io.github.jan.supabase.postgrest.rpc
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -11,6 +12,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -22,11 +24,19 @@ import kotlinx.serialization.json.encodeToJsonElement
|
|||
import kotlinx.serialization.json.put
|
||||
|
||||
@Serializable
|
||||
private data class AddonSyncItem(
|
||||
@SerialName("manifest_url") val manifestUrl: String,
|
||||
private data class AddonRow(
|
||||
val url: String,
|
||||
val name: String? = null,
|
||||
val enabled: Boolean = true,
|
||||
@SerialName("sort_order") val sortOrder: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class AddonPushItem(
|
||||
val url: String,
|
||||
val name: String = "",
|
||||
val enabled: Boolean = true,
|
||||
@SerialName("sort_order") val sortOrder: Int = 0,
|
||||
@SerialName("display_name") val displayName: String = "",
|
||||
@SerialName("is_enabled") val isEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
object AddonRepository {
|
||||
|
|
@ -37,12 +47,15 @@ object AddonRepository {
|
|||
val uiState: StateFlow<AddonsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var initialized = false
|
||||
private var pulledFromServer = false
|
||||
|
||||
fun initialize() {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
log.d { "initialize() — loading local addons" }
|
||||
|
||||
val storedUrls = AddonStorage.loadInstalledAddonUrls()
|
||||
log.d { "initialize() — local addon count: ${storedUrls.size}" }
|
||||
if (storedUrls.isEmpty()) return
|
||||
|
||||
_uiState.value = AddonsUiState(
|
||||
|
|
@ -58,27 +71,70 @@ object AddonRepository {
|
|||
}
|
||||
|
||||
suspend fun pullFromServer(profileId: Int) {
|
||||
log.i { "pullFromServer() — profileId=$profileId, initialized=$initialized, pulledFromServer=$pulledFromServer" }
|
||||
runCatching {
|
||||
val params = buildJsonObject { put("p_profile_id", profileId) }
|
||||
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_addons", params)
|
||||
val serverAddons = result.decodeList<AddonSyncItem>()
|
||||
val urls = serverAddons.sortedBy { it.sortOrder }.map { it.manifestUrl }
|
||||
if (urls.isNotEmpty()) {
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = urls.map { url ->
|
||||
ManagedAddon(manifestUrl = url, isRefreshing = true)
|
||||
},
|
||||
)
|
||||
persist()
|
||||
urls.forEach(::refreshAddon)
|
||||
val rows = SupabaseProvider.client.postgrest
|
||||
.from("addons")
|
||||
.select {
|
||||
filter { eq("profile_id", profileId) }
|
||||
order("sort_order", Order.ASCENDING)
|
||||
}
|
||||
.decodeList<AddonRow>()
|
||||
|
||||
val urls = rows.map { ensureManifestSuffix(it.url) }
|
||||
log.i { "pullFromServer() — server returned ${rows.size} addons" }
|
||||
urls.forEachIndexed { i, u -> log.d { " server[$i]: $u" } }
|
||||
|
||||
if (urls.isEmpty() && !pulledFromServer) {
|
||||
val localUrls = AddonStorage.loadInstalledAddonUrls()
|
||||
log.i { "pullFromServer() — server empty, local has ${localUrls.size} addons" }
|
||||
if (localUrls.isNotEmpty()) {
|
||||
log.i { "pullFromServer() — migrating local addons to server for profile $profileId" }
|
||||
initialize()
|
||||
pulledFromServer = true
|
||||
val addons = localUrls.mapIndexed { index, addonUrl ->
|
||||
AddonPushItem(
|
||||
url = addonUrl,
|
||||
name = _uiState.value.addons
|
||||
.find { it.manifestUrl == addonUrl }?.manifest?.name ?: "",
|
||||
enabled = true,
|
||||
sortOrder = index,
|
||||
)
|
||||
}
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_addons", json.encodeToJsonElement(addons))
|
||||
}
|
||||
SupabaseProvider.client.postgrest.rpc("sync_push_addons", params)
|
||||
log.i { "pullFromServer() — migration push done (${addons.size} addons)" }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = urls.map { url ->
|
||||
ManagedAddon(manifestUrl = url, isRefreshing = true)
|
||||
},
|
||||
)
|
||||
persist()
|
||||
urls.forEach(::refreshAddon)
|
||||
pulledFromServer = true
|
||||
initialized = true
|
||||
log.i { "pullFromServer() — applied ${urls.size} addons to state" }
|
||||
}.onFailure { e ->
|
||||
log.e(e) { "Failed to pull addons from server" }
|
||||
log.e(e) { "pullFromServer() — FAILED" }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitManifestsLoaded() {
|
||||
if (_uiState.value.addons.isEmpty()) return
|
||||
uiState.first { state ->
|
||||
state.addons.isEmpty() || state.addons.any { it.manifest != null }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addAddon(rawUrl: String): AddAddonResult {
|
||||
log.i { "addAddon() — rawUrl=$rawUrl" }
|
||||
val manifestUrl = try {
|
||||
normalizeManifestUrl(rawUrl)
|
||||
} catch (error: IllegalArgumentException) {
|
||||
|
|
@ -117,6 +173,7 @@ object AddonRepository {
|
|||
}
|
||||
|
||||
fun removeAddon(manifestUrl: String) {
|
||||
log.i { "removeAddon() — $manifestUrl" }
|
||||
_uiState.update { current ->
|
||||
current.copy(
|
||||
addons = current.addons.filterNot { it.manifestUrl == manifestUrl },
|
||||
|
|
@ -176,20 +233,22 @@ object AddonRepository {
|
|||
runCatching {
|
||||
val profileId = ProfileRepository.activeProfileId
|
||||
val addons = _uiState.value.addons.mapIndexed { index, addon ->
|
||||
AddonSyncItem(
|
||||
manifestUrl = addon.manifestUrl,
|
||||
AddonPushItem(
|
||||
url = addon.manifestUrl,
|
||||
name = addon.manifest?.name ?: "",
|
||||
enabled = true,
|
||||
sortOrder = index,
|
||||
displayName = addon.manifest?.name ?: "",
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
log.d { "pushToServer() — profileId=$profileId, pushing ${addons.size} addons" }
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_addons", json.encodeToJsonElement(addons))
|
||||
}
|
||||
SupabaseProvider.client.postgrest.rpc("sync_push_addons", params)
|
||||
log.d { "pushToServer() — success" }
|
||||
}.onFailure { e ->
|
||||
log.e(e) { "Failed to push addons to server" }
|
||||
log.e(e) { "pushToServer() — FAILED" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -218,6 +277,13 @@ object AddonRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ensureManifestSuffix(url: String): String {
|
||||
val path = url.substringBefore("?").trimEnd('/')
|
||||
val query = url.substringAfter("?", "")
|
||||
val withSuffix = if (path.endsWith("/manifest.json")) path else "$path/manifest.json"
|
||||
return if (query.isEmpty()) withSuffix else "$withSuffix?$query"
|
||||
}
|
||||
|
||||
private fun normalizeManifestUrl(rawUrl: String): String {
|
||||
val trimmed = rawUrl.trim()
|
||||
require(trimmed.isNotEmpty()) { "Enter an addon URL." }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.nuvio.app.features.watchprogress
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.player.PlayerPlaybackSnapshot
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
|
|
@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -114,6 +116,13 @@ object WatchProgressRepository {
|
|||
if (needsResolution.isEmpty()) return
|
||||
|
||||
syncScope.launch {
|
||||
withTimeoutOrNull(30_000L) {
|
||||
AddonRepository.awaitManifestsLoaded()
|
||||
} ?: run {
|
||||
log.w { "Timed out waiting for addon manifests" }
|
||||
return@launch
|
||||
}
|
||||
|
||||
for ((key, entries) in needsResolution) {
|
||||
val (metaId, metaType) = key
|
||||
val meta = runCatching {
|
||||
|
|
|
|||
Loading…
Reference in a new issue