From 1039bfa5d33452c2e2aec20864c8e95f43de92fa Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:14:09 +0300 Subject: [PATCH] Pull Stremio's library as a timestamped watchlist and reconcile it LibraryItem never declared removed/_mtime so those fields were silently dropped by Gson before Rust ever saw them, even though library_state.rs already worked on raw JSON and could have used them. Adds the fields, StremioRepository.getLibraryWatchlistWithTimestamps, and wires it into the same mergeWatchlist reconciliation the other providers use -- but pull-only (reconcileWatchlistPullOnly), not the full push-capable path. Deliberately not pushing local watchlist changes to Stremio: that would need a "toggle membership" write that preserves whatever progress/state fields already exist on the remote item (a read-modify-write), which isn't built and isn't safe to guess at without being able to verify Stremio's actual datastore PUT semantics (full-replace vs partial-patch) against a live account. --- .../app/ui/catalog/HomeLibraryCoordinator.kt | 23 +++++++++++++++++ .../fluxa/app/core/rust/FluxaCoreNative.kt | 4 +++ .../app/data/repository/StremioRepository.kt | 25 +++++++++++++++++++ .../fluxa/app/data/remote/StremioModels.kt | 4 ++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt index 418f1a1..91118e5 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt @@ -75,6 +75,10 @@ internal class HomeLibraryCoordinator( .getOrDefault(emptyList()) } ?: emptyList() } + val stremioAuthKey = profile?.authKey?.takeIf { !profile.isGuest } + val stremioWatchlistWithTimestamps = stremioAuthKey?.let { authKey -> + async(Dispatchers.IO) { repository.getLibraryWatchlistWithTimestamps(authKey) } + } val traktToken = profile?.traktAccessToken val traktPlannedWithListedAt = if (!traktToken.isNullOrBlank()) async(Dispatchers.IO) { traktRepository.getTraktWatchlistWithListedAt(traktToken) } else null @@ -142,6 +146,13 @@ internal class HomeLibraryCoordinator( .onFailure { Log.w("HomeLibrary", "AniList watchlist reconcile failed", it) } } } + if (stremioAuthKey != null) { + scope.launch(Dispatchers.IO) { + val remoteWatchlist = stremioWatchlistWithTimestamps?.await().orEmpty() + runCatching { reconcileWatchlistPullOnly(remoteWatchlist) } + .onFailure { Log.w("HomeLibrary", "Stremio watchlist reconcile failed", it) } + } + } } catch (e: Exception) { Log.w("HomeLibrary", "Failed to load library data", e) setLibraryState(_state.value.copy( @@ -174,6 +185,18 @@ internal class HomeLibraryCoordinator( } } + private suspend fun reconcileWatchlistPullOnly(remoteEntries: List>) { + val localSnapshot = watchlistManager.getWatchlistMembershipSnapshot() + val remoteMembership = remoteEntries.map { (meta, updatedAtMs) -> + ExternalSyncMergeBridge.RemoteMembershipItem(meta.id, updatedAtMs) + } + val plan = ExternalSyncMergeBridge.mergeWatchlist(localSnapshot, remoteMembership) + val remoteById = remoteEntries.associateBy { it.first.id } + plan.applyLocalAdd.forEach { id -> + remoteById[id]?.first?.let { watchlistManager.applyRemoteWatchlistAdd(it) } + } + } + private suspend fun reconcileWatchedEpisodes(profile: UserProfile, remoteWatched: Map) { val localSnapshot = watchlistManager.getWatchedEpisodeMembershipSnapshot() val remoteMembership = remoteWatched.map { (videoId, watchedAtMs) -> diff --git a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt index 90758de..60dbfae 100644 --- a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt +++ b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt @@ -1781,6 +1781,10 @@ object FluxaCoreNative { return value.takeUnless { it.isJsonNull }?.let { gson.fromJson>(it, metaListType) } ?: emptyList() } + fun libraryWatchlistItems(items: List): com.google.gson.JsonElement { + return FluxaCoreUniFfi.coreInvokeValue("libraryWatchlistItems", gson.toJson(items)) + } + fun filterHomeContinueWatching(items: List, traktWatchedState: TraktWatchedState): List { val args = JsonObject().apply { addProperty("itemsJson", gson.toJson(items)) diff --git a/data/src/androidMain/kotlin/com/fluxa/app/data/repository/StremioRepository.kt b/data/src/androidMain/kotlin/com/fluxa/app/data/repository/StremioRepository.kt index 4625e6a..5f7705c 100644 --- a/data/src/androidMain/kotlin/com/fluxa/app/data/repository/StremioRepository.kt +++ b/data/src/androidMain/kotlin/com/fluxa/app/data/repository/StremioRepository.kt @@ -211,6 +211,31 @@ class StremioRepository @Inject constructor( } } + suspend fun getLibraryWatchlistWithTimestamps(authKey: String): List> = withContext(Dispatchers.IO) { + try { + val response = authService.getDatastore(DatastoreRequest(authKey, "library")) + val value = FluxaCoreNative.libraryWatchlistItems(response.body()?.result.orEmpty()) + if (value.isJsonNull) return@withContext emptyList() + value.asJsonArray.mapNotNull { entry -> + val item = entry.asJsonObject + val id = item.get("id")?.takeUnless { it.isJsonNull }?.asString ?: return@mapNotNull null + val updatedAtMs = item.get("updatedAtMs")?.takeUnless { it.isJsonNull }?.asLong ?: return@mapNotNull null + val meta = Meta( + id = id, + name = item.get("name")?.takeUnless { it.isJsonNull }?.asString ?: id, + type = item.get("type")?.takeUnless { it.isJsonNull }?.asString ?: "", + poster = item.get("poster")?.takeUnless { it.isJsonNull }?.asString, + background = item.get("background")?.takeUnless { it.isJsonNull }?.asString, + reason = "Stremio" + ) + meta to updatedAtMs + } + } catch (e: Exception) { + failureReporter.report("stremio.library.getWatchlist", e) + emptyList() + } + } + suspend fun getWatchedVideoIds(authKey: String, imdbId: String): List = withContext(Dispatchers.IO) { try { val response = authService.getDatastore(DatastoreRequest(authKey, "library")) diff --git a/data/src/commonMain/kotlin/com/fluxa/app/data/remote/StremioModels.kt b/data/src/commonMain/kotlin/com/fluxa/app/data/remote/StremioModels.kt index 9b8242f..94202f9 100644 --- a/data/src/commonMain/kotlin/com/fluxa/app/data/remote/StremioModels.kt +++ b/data/src/commonMain/kotlin/com/fluxa/app/data/remote/StremioModels.kt @@ -20,7 +20,9 @@ data class LibraryItem( val background: String? = null, val logo: String? = null, val state: LibraryItemState? = null, - val lastWatched: String? = null + val lastWatched: String? = null, + val removed: Boolean? = null, + val _mtime: String? = null ) { val id: String get() = _id }