feat: catalog order to be cross-platform

This commit is contained in:
tapframe 2026-06-11 13:00:46 +05:30
parent 850988bf87
commit 5249614b50
2 changed files with 134 additions and 20 deletions

View file

@ -1,3 +1,5 @@
package com.nuvio.app.core.sync
internal const val MOBILE_SYNC_PLATFORM = "mobile"
internal const val HOME_CATALOG_SHARED_SYNC_PLATFORM = "home_catalog_shared"
internal val HOME_CATALOG_LEGACY_SYNC_PLATFORMS = listOf(MOBILE_SYNC_PLATFORM, "tv")

View file

@ -3,7 +3,8 @@ package com.nuvio.app.features.home
import co.touchlab.kermit.Logger
import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.sync.MOBILE_SYNC_PLATFORM
import com.nuvio.app.core.sync.HOME_CATALOG_LEGACY_SYNC_PLATFORMS
import com.nuvio.app.core.sync.HOME_CATALOG_SHARED_SYNC_PLATFORM
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.features.profiles.ProfileRepository
import io.github.jan.supabase.postgrest.postgrest
@ -25,6 +26,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.put
@Serializable
@ -53,6 +55,14 @@ private data class SupabaseHomeCatalogSettingsBlob(
@SerialName("updated_at") val updatedAt: String? = null,
)
private data class RemoteHomeCatalogSettings(
val platform: String,
val payload: SyncHomeCatalogPayload,
val updatedAt: String?,
val hasHideUnreleasedContent: Boolean,
val hasHideCatalogUnderline: Boolean,
)
object HomeCatalogSettingsSyncService {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val log = Logger.withTag("HomeCatalogSettingsSyncService")
@ -62,6 +72,8 @@ object HomeCatalogSettingsSyncService {
}
private const val PUSH_DEBOUNCE_MS = 1500L
private const val HIDE_UNRELEASED_CONTENT_KEY = "hide_unreleased_content"
private const val HIDE_CATALOG_UNDERLINE_KEY = "hide_catalog_underline"
@Volatile
var isSyncingFromRemote: Boolean = false
@ -76,31 +88,18 @@ object HomeCatalogSettingsSyncService {
suspend fun pullFromServer(profileId: Int) {
runCatching {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_platform", MOBILE_SYNC_PLATFORM)
}
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_home_catalog_settings", params)
val blobs = result.decodeList<SupabaseHomeCatalogSettingsBlob>()
val blob = blobs.firstOrNull()
val localPayload = HomeCatalogSettingsRepository.exportToSyncPayload()
val remote = fetchBestRemotePayload(profileId, localPayload)
if (blob == null) {
if (remote == null) {
log.i { "pullFromServer — no remote home catalog settings found" }
val localPayload = HomeCatalogSettingsRepository.exportToSyncPayload()
if (localPayload.items.isNotEmpty()) {
pushToRemote(profileId)
}
return
}
val remotePayload = runCatching {
json.decodeFromJsonElement(SyncHomeCatalogPayload.serializer(), blob.settingsJson)
}.getOrNull()
if (remotePayload == null) {
log.w { "pullFromServer — failed to parse remote home catalog settings" }
return
}
val remotePayload = remote.payload
if (remotePayload.items.isEmpty()) {
log.i { "pullFromServer — remote has empty items, preserving local catalog order" }
@ -118,6 +117,9 @@ object HomeCatalogSettingsSyncService {
HomeCatalogSettingsRepository.applyFromRemote(remotePayload)
isSyncingFromRemote = false
log.i { "pullFromServer — applied ${remotePayload.items.size} items from remote" }
if (remote.platform != HOME_CATALOG_SHARED_SYNC_PLATFORM) {
pushToRemote(profileId)
}
}.onFailure { e ->
isSyncingFromRemote = false
log.e(e) { "pullFromServer — FAILED" }
@ -142,11 +144,11 @@ object HomeCatalogSettingsSyncService {
private suspend fun pushToRemote(profileId: Int) {
runCatching {
val payload = HomeCatalogSettingsRepository.exportToSyncPayload()
val jsonElement = json.encodeToJsonElement(SyncHomeCatalogPayload.serializer(), payload)
val jsonElement = mergedSharedPayloadJson(profileId, payload)
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_platform", MOBILE_SYNC_PLATFORM)
put("p_platform", HOME_CATALOG_SHARED_SYNC_PLATFORM)
put("p_settings_json", jsonElement)
}
SupabaseProvider.client.postgrest.rpc("sync_push_home_catalog_settings", params)
@ -172,4 +174,114 @@ object HomeCatalogSettingsSyncService {
}
}
}
private suspend fun fetchBestRemotePayload(
profileId: Int,
localPayload: SyncHomeCatalogPayload,
): RemoteHomeCatalogSettings? {
val shared = fetchRemotePayload(
profileId = profileId,
platform = HOME_CATALOG_SHARED_SYNC_PLATFORM,
localPayload = localPayload,
)
val legacyRows = HOME_CATALOG_LEGACY_SYNC_PLATFORMS
.mapNotNull { platform ->
fetchRemotePayload(
profileId = profileId,
platform = platform,
localPayload = localPayload,
)
}
val rows = listOfNotNull(shared) + legacyRows
val selected = rows
.filter { it.payload.items.isNotEmpty() }
.maxByOrNull { it.updatedAt.orEmpty() }
?: shared
?: legacyRows.maxByOrNull { it.updatedAt.orEmpty() }
return selected?.withNewestStandaloneSettings(rows)
}
private suspend fun fetchRemotePayload(
profileId: Int,
platform: String,
localPayload: SyncHomeCatalogPayload,
): RemoteHomeCatalogSettings? {
val blob = fetchRemoteBlob(profileId, platform) ?: return null
val payload = decodePayloadPreservingLocalDefaults(blob.settingsJson, localPayload)
if (payload == null) {
log.w { "pullFromServer — failed to parse remote home catalog settings for platform=$platform" }
return null
}
return RemoteHomeCatalogSettings(
platform = platform,
payload = payload,
updatedAt = blob.updatedAt,
hasHideUnreleasedContent = blob.settingsJson.containsKey(HIDE_UNRELEASED_CONTENT_KEY),
hasHideCatalogUnderline = blob.settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY),
)
}
private fun RemoteHomeCatalogSettings.withNewestStandaloneSettings(
rows: List<RemoteHomeCatalogSettings>,
): RemoteHomeCatalogSettings {
val hideUnreleasedSource = rows
.filter { it.hasHideUnreleasedContent }
.maxByOrNull { it.updatedAt.orEmpty() }
val hideUnderlineSource = rows
.filter { it.hasHideCatalogUnderline }
.maxByOrNull { it.updatedAt.orEmpty() }
return copy(
payload = payload.copy(
hideUnreleasedContent = hideUnreleasedSource?.payload?.hideUnreleasedContent
?: payload.hideUnreleasedContent,
hideCatalogUnderline = hideUnderlineSource?.payload?.hideCatalogUnderline
?: payload.hideCatalogUnderline,
),
)
}
private suspend fun fetchRemoteBlob(
profileId: Int,
platform: String,
): SupabaseHomeCatalogSettingsBlob? {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_platform", platform)
}
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_home_catalog_settings", params)
return result.decodeList<SupabaseHomeCatalogSettingsBlob>().firstOrNull()
}
private fun decodePayloadPreservingLocalDefaults(
settingsJson: JsonObject,
localPayload: SyncHomeCatalogPayload,
): SyncHomeCatalogPayload? = runCatching {
val decoded = json.decodeFromJsonElement(SyncHomeCatalogPayload.serializer(), settingsJson)
decoded.copy(
hideUnreleasedContent = if (settingsJson.containsKey(HIDE_UNRELEASED_CONTENT_KEY)) {
decoded.hideUnreleasedContent
} else {
localPayload.hideUnreleasedContent
},
hideCatalogUnderline = if (settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY)) {
decoded.hideCatalogUnderline
} else {
localPayload.hideCatalogUnderline
},
)
}.getOrNull()
private suspend fun mergedSharedPayloadJson(
profileId: Int,
payload: SyncHomeCatalogPayload,
): JsonObject {
val localJson = json.encodeToJsonElement(SyncHomeCatalogPayload.serializer(), payload).jsonObject
val remoteJson = fetchRemoteBlob(profileId, HOME_CATALOG_SHARED_SYNC_PLATFORM)?.settingsJson
return buildJsonObject {
remoteJson?.forEach { (key, value) -> put(key, value) }
localJson.forEach { (key, value) -> put(key, value) }
}
}
}