mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-06 08:48:51 +00:00
Pull AniList's watchlist (PLANNING status) and reconcile it by timestamp
The AniList GraphQL query only ever fetched status: CURRENT, so the watchlist-add push added in the AniList phase had nothing to reconcile against on the next sync -- pushed items would just get re-pushed or silently diverge. Drops the CURRENT-only filter (Rust's anilist_entries_to_sync already builds a timestamped watchlist array from PLANNING entries, just needed the query to actually fetch them) and wires it through the same mergeWatchlist reconciliation Trakt uses. Generalizes reconcileTraktWatchlist -> reconcileWatchlist since the logic is fully provider-agnostic.
This commit is contained in:
parent
32e7774056
commit
edc69261e0
3 changed files with 50 additions and 14 deletions
|
|
@ -93,6 +93,9 @@ internal class HomeLibraryCoordinator(
|
|||
val simklCompleted = if (!simklToken.isNullOrBlank()) async(Dispatchers.IO) { repository.getSimklLibraryItems(simklToken, "completed") } else null
|
||||
val simklWatchedEpisodesWithTimestamps = if (!simklToken.isNullOrBlank()) async(Dispatchers.IO) { repository.getSimklWatchedEpisodesWithTimestamps(simklToken) } else null
|
||||
|
||||
val anilistToken = profile?.anilistAccessToken
|
||||
val anilistWatchlistWithTimestamps = if (!anilistToken.isNullOrBlank()) async(Dispatchers.IO) { repository.getAnilistWatchlistWithTimestamps(anilistToken) } else null
|
||||
|
||||
val traktPlannedWithTimestamps = traktPlannedWithListedAt?.await().orEmpty()
|
||||
|
||||
setLibraryState(LibraryUiState(
|
||||
|
|
@ -111,7 +114,7 @@ internal class HomeLibraryCoordinator(
|
|||
|
||||
if (profile != null && !traktToken.isNullOrBlank()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
runCatching { reconcileTraktWatchlist(profile, traktPlannedWithTimestamps) }
|
||||
runCatching { reconcileWatchlist(profile, traktPlannedWithTimestamps) }
|
||||
.onFailure { Log.w("HomeLibrary", "Trakt watchlist reconcile failed", it) }
|
||||
}
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
|
@ -132,6 +135,13 @@ internal class HomeLibraryCoordinator(
|
|||
.onFailure { Log.w("HomeLibrary", "Simkl watched reconcile failed", it) }
|
||||
}
|
||||
}
|
||||
if (profile != null && !anilistToken.isNullOrBlank()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val remotePlanned = anilistWatchlistWithTimestamps?.await().orEmpty()
|
||||
runCatching { reconcileWatchlist(profile, remotePlanned) }
|
||||
.onFailure { Log.w("HomeLibrary", "AniList watchlist reconcile failed", it) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("HomeLibrary", "Failed to load library data", e)
|
||||
setLibraryState(_state.value.copy(
|
||||
|
|
@ -143,7 +153,7 @@ internal class HomeLibraryCoordinator(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun reconcileTraktWatchlist(profile: UserProfile, remoteEntries: List<Pair<Meta, Long>>) {
|
||||
private suspend fun reconcileWatchlist(profile: UserProfile, remoteEntries: List<Pair<Meta, Long>>) {
|
||||
val localSnapshot = watchlistManager.getWatchlistMembershipSnapshot()
|
||||
val remoteMembership = remoteEntries.map { (meta, listedAtMs) ->
|
||||
ExternalSyncMergeBridge.RemoteMembershipItem(meta.id, listedAtMs)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ import javax.inject.Singleton
|
|||
|
||||
private const val EPISODE_PROGRESS_UNIT_MS = 45 * 60_000L
|
||||
|
||||
private const val ANILIST_CURRENT_LIST_QUERY = """
|
||||
private const val ANILIST_LIST_QUERY = """
|
||||
query (${'$'}userId: Int) {
|
||||
MediaListCollection(userId: ${'$'}userId, type: ANIME, status: CURRENT) {
|
||||
MediaListCollection(userId: ${'$'}userId, type: ANIME) {
|
||||
lists {
|
||||
entries {
|
||||
status
|
||||
|
|
@ -322,8 +322,8 @@ class ExternalLibraryClient @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun getAnilistContinueWatchingItems(token: String?): List<Meta> {
|
||||
if (token.isNullOrBlank()) return emptyList()
|
||||
private suspend fun anilistSyncValue(token: String?): com.google.gson.JsonObject? {
|
||||
if (token.isNullOrBlank()) return null
|
||||
val authorization = "Bearer $token"
|
||||
return try {
|
||||
val viewerResponse = traktApi.anilistGraphQl(
|
||||
|
|
@ -335,12 +335,12 @@ class ExternalLibraryClient @Inject constructor(
|
|||
?.getAsJsonObject("Viewer")
|
||||
?.get("id")
|
||||
?.takeUnless { it.isJsonNull }
|
||||
?.asInt ?: return emptyList()
|
||||
?.asInt ?: return null
|
||||
|
||||
val listResponse = traktApi.anilistGraphQl(
|
||||
authorization,
|
||||
AnilistGraphQlRequest(
|
||||
query = ANILIST_CURRENT_LIST_QUERY,
|
||||
query = ANILIST_LIST_QUERY,
|
||||
variables = mapOf("userId" to viewerId)
|
||||
)
|
||||
)
|
||||
|
|
@ -353,7 +353,7 @@ class ExternalLibraryClient @Inject constructor(
|
|||
lists.forEach { list ->
|
||||
list.asJsonObject.getAsJsonArray("entries")?.forEach(entries::add)
|
||||
}
|
||||
if (entries.size() == 0) return emptyList()
|
||||
if (entries.size() == 0) return null
|
||||
|
||||
val args = com.google.gson.JsonObject().apply {
|
||||
add("entries", entries)
|
||||
|
|
@ -362,15 +362,38 @@ class ExternalLibraryClient @Inject constructor(
|
|||
val envelope = JsonParser.parseString(
|
||||
FluxaCoreUniFfi.coreInvoke("anilistEntriesToSync", args.toString())
|
||||
).asJsonObject
|
||||
if (envelope.get("ok")?.asBoolean != true) return emptyList()
|
||||
val progress = envelope.getAsJsonObject("value")?.getAsJsonObject("progress") ?: return emptyList()
|
||||
progress.entrySet().mapNotNull { (_, value) -> anilistProgressEntryToMeta(value.asJsonObject) }
|
||||
if (envelope.get("ok")?.asBoolean != true) return null
|
||||
envelope.getAsJsonObject("value")
|
||||
} catch (e: Exception) {
|
||||
Log.w("ExternalLibraryClient", "Failed to load AniList continue watching items", e)
|
||||
emptyList()
|
||||
Log.w("ExternalLibraryClient", "Failed to load AniList sync data", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAnilistContinueWatchingItems(token: String?): List<Meta> {
|
||||
val progress = anilistSyncValue(token)?.getAsJsonObject("progress") ?: return emptyList()
|
||||
return progress.entrySet().mapNotNull { (_, value) -> anilistProgressEntryToMeta(value.asJsonObject) }
|
||||
}
|
||||
|
||||
suspend fun getAnilistWatchlistWithTimestamps(token: String?): List<Pair<Meta, Long>> {
|
||||
val watchlist = anilistSyncValue(token)?.getAsJsonArray("watchlist") ?: return emptyList()
|
||||
return watchlist.mapNotNull { entry -> anilistWatchlistEntryToMeta(entry.asJsonObject) }
|
||||
}
|
||||
|
||||
private fun anilistWatchlistEntryToMeta(item: com.google.gson.JsonObject): Pair<Meta, Long>? {
|
||||
val id = item.get("id")?.takeUnless { it.isJsonNull }?.asString ?: return null
|
||||
val updatedAtMs = item.get("updatedAtMs")?.takeUnless { it.isJsonNull }?.asLong ?: return null
|
||||
val meta = Meta(
|
||||
id = id,
|
||||
name = item.get("name")?.takeUnless { it.isJsonNull }?.asString ?: id,
|
||||
type = item.get("type")?.takeUnless { it.isJsonNull }?.asString ?: "series",
|
||||
poster = item.get("poster")?.takeUnless { it.isJsonNull }?.asString,
|
||||
background = item.get("background")?.takeUnless { it.isJsonNull }?.asString,
|
||||
reason = "AniList"
|
||||
)
|
||||
return meta to updatedAtMs
|
||||
}
|
||||
|
||||
private fun anilistProgressEntryToMeta(entry: com.google.gson.JsonObject): Meta? {
|
||||
val meta = entry.getAsJsonObject("meta") ?: return null
|
||||
val id = meta.get("id")?.takeUnless { it.isJsonNull }?.asString ?: return null
|
||||
|
|
|
|||
|
|
@ -195,6 +195,9 @@ class StremioRepository @Inject constructor(
|
|||
suspend fun getSimklWatchedEpisodesWithTimestamps(token: String?): Map<String, Long> =
|
||||
externalLibraryClient.getSimklWatchedEpisodesWithTimestamps(token)
|
||||
|
||||
suspend fun getAnilistWatchlistWithTimestamps(token: String?): List<Pair<Meta, Long>> =
|
||||
externalLibraryClient.getAnilistWatchlistWithTimestamps(token)
|
||||
|
||||
suspend fun clearTraktPlaybackProgress(token: String?, meta: Meta) =
|
||||
traktRepository.clearPlaybackProgress(token, meta)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue