From 8e04cbae31b45c8e1f5aebf368208bacdd104ea3 Mon Sep 17 00:00:00 2001 From: silentbil Date: Sun, 21 Jun 2026 23:17:05 +0300 Subject: [PATCH] cloud sync timing fix --- .../tv/data/repository/CloudSyncRepository.kt | 38 ++++++++++++++++--- .../tv/data/repository/IptvRepository.kt | 10 +++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt index 15ed5841..e9af9d22 100644 --- a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt +++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt @@ -747,8 +747,14 @@ class CloudSyncRepository @Inject constructor( ) } + val effectivePayload = if (existingRemotePayload != null && !iptvRepository.isGroupOrderLocallyDirty()) { + mergeRemoteGroupOrder(payload, existingRemotePayload) + } else { + payload + } + val payloadHash = runCatching { - JSONObject(payload).apply { remove("updatedAt") }.toString().hashCode() + JSONObject(effectivePayload).apply { remove("updatedAt") }.toString().hashCode() }.getOrNull() if (!force && payloadHash != null && payloadHash == lastPushedPayloadHash && !isPushDirty && pushFailureCount == 0) { @@ -760,15 +766,15 @@ class CloudSyncRepository @Inject constructor( return Result.success(Unit) } - val result = authRepository.saveAccountSyncPayload(payload) + val result = authRepository.saveAccountSyncPayload(effectivePayload) if (result.isSuccess) { clearLocalDirtyAfterSuccessfulPush() lastPushedPayloadHash = payloadHash pushFailureCount = 0 - Log.i(TAG, "Push succeeded size=${payloadSizeBucket(payload)}") + Log.i(TAG, "Push succeeded size=${payloadSizeBucket(effectivePayload)}") AppLogger.breadcrumb( tag = "CloudSync", - message = "push_success size=${payloadSizeBucket(payload)} user=${userId.take(8)}", + message = "push_success size=${payloadSizeBucket(effectivePayload)} user=${userId.take(8)}", severity = "info" ) onPushCompleted?.invoke() @@ -780,7 +786,7 @@ class CloudSyncRepository @Inject constructor( pushFailureCount++ Log.w( TAG, - "Push failed size=${payloadSizeBucket(payload)} failures=$pushFailureCount error=${result.exceptionOrNull()?.message}" + "Push failed size=${payloadSizeBucket(effectivePayload)} failures=$pushFailureCount error=${result.exceptionOrNull()?.message}" ) AppLogger.recordException( throwable = result.exceptionOrNull() ?: IllegalStateException("Cloud push failed"), @@ -788,7 +794,7 @@ class CloudSyncRepository @Inject constructor( "error_area" to "CloudSync", "cloud_flow" to "push_save_payload", "dirty" to isPushDirty.toString(), - "payload_size" to payloadSizeBucket(payload), + "payload_size" to payloadSizeBucket(effectivePayload), "failure_count" to pushFailureCount.toString() ) ) @@ -796,6 +802,26 @@ class CloudSyncRepository @Inject constructor( return result } + private fun mergeRemoteGroupOrder(localPayload: String, remotePayload: String): String { + return runCatching { + val local = JSONObject(localPayload) + val remote = JSONObject(remotePayload) + val localByProfile = local.optJSONObject("iptvByProfile") ?: return@runCatching localPayload + val remoteByProfile = remote.optJSONObject("iptvByProfile") ?: return@runCatching localPayload + val remoteKeys = remoteByProfile.keys() + while (remoteKeys.hasNext()) { + val profileId = remoteKeys.next() + val remoteProfile = remoteByProfile.optJSONObject(profileId) ?: continue + val localProfile = localByProfile.optJSONObject(profileId) ?: continue + val remoteGroupOrder = remoteProfile.optJSONArray("groupOrder") ?: continue + if (remoteGroupOrder.length() > 0) { + localProfile.put("groupOrder", remoteGroupOrder) + } + } + local.toString() + }.getOrDefault(localPayload) + } + // ══════════════════════════════════════════════════════════ // PULL CLOUD STATE TO LOCAL // ══════════════════════════════════════════════════════════ diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt index a1fa4067..38c600b9 100644 --- a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt +++ b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt @@ -206,6 +206,11 @@ class IptvRepository @Inject constructor( private var cachedGroupedChannels: Map> = emptyMap() @Volatile var cachedStalkerApi: com.arflix.tv.data.api.StalkerApi? = null + @Volatile + private var groupOrderLocallyDirty = false + + fun isGroupOrderLocallyDirty(): Boolean = groupOrderLocallyDirty + @Volatile private var cachedNowNext: ConcurrentHashMap = ConcurrentHashMap() private val emptyShortEpgCooldownUntil = ConcurrentHashMap() @@ -1390,6 +1395,7 @@ class IptvRepository @Inject constructor( if (idx > 0) { order.removeAt(idx); order.add(idx - 1, target) } prefs[groupOrderKey()] = gson.toJson(order) } + groupOrderLocallyDirty = true invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "move group up") } @@ -1405,6 +1411,7 @@ class IptvRepository @Inject constructor( order.add(0, target) prefs[groupOrderKey()] = gson.toJson(order) } + groupOrderLocallyDirty = true invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "move group to top") } @@ -1419,6 +1426,7 @@ class IptvRepository @Inject constructor( if (idx >= 0 && idx < order.size - 1) { order.removeAt(idx); order.add(idx + 1, target) } prefs[groupOrderKey()] = gson.toJson(order) } + groupOrderLocallyDirty = true invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "move group down") } @@ -1428,6 +1436,7 @@ class IptvRepository @Inject constructor( existing.removeAll { PlaylistGroupKey(it).playlistId == playlistId } prefs[groupOrderKey()] = gson.toJson(existing) } + groupOrderLocallyDirty = true invalidationBus.markDirty(CloudSyncScope.IPTV, profileManager.getProfileIdSync(), "reset group order") } @@ -2946,6 +2955,7 @@ class IptvRepository @Inject constructor( prefs.remove(tvSessionKeyFor(safeProfileId)) } } + groupOrderLocallyDirty = false if (profileManager.getProfileIdSync() == safeProfileId) { invalidateCache() }