From 275727ed904413fb2acbd5428e29e7894221c372 Mon Sep 17 00:00:00 2001 From: Zeedif Date: Tue, 9 Sep 2025 16:13:05 -0600 Subject: [PATCH] feat(kosync): add mutations for manual progress push and pull (#1625) Exposes the existing push and pull functionality from the KoreaderSyncService via the GraphQL API. This change introduces two new mutations: - `pushKoSyncProgress`: Manually sends the current chapter's reading progress to the KOReader sync server. - `pullKoSyncProgress`: Manually fetches and applies the latest reading progress from the KOReader sync server. These mutations enable clients and WebUIs to implement manual sync triggers, providing users with more direct control over their reading progress synchronization, similar to the functionality offered by the official KOReader plugin and other clients like Readest. --- .../graphql/mutations/KoreaderSyncMutation.kt | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt index bb4da763..71d933e1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt @@ -1,14 +1,21 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.execution.DataFetcherResult import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update +import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.server.getAttribute +import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload import suwayomi.tachidesk.graphql.types.SettingsType +import suwayomi.tachidesk.graphql.types.SyncConflictInfoType import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService +import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future -import suwayomi.tachidesk.server.JavalinSetup.getAttribute import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture @@ -53,4 +60,100 @@ class KoreaderSyncMutation { settings = SettingsType(), ) } + + data class PushKoSyncProgressInput( + val clientMutationId: String? = null, + val chapterId: Int, + ) + + data class PushKoSyncProgressPayload( + val clientMutationId: String?, + val success: Boolean, + val chapter: ChapterType?, + ) + + fun pushKoSyncProgress( + dataFetchingEnvironment: DataFetchingEnvironment, + input: PushKoSyncProgressInput, + ): CompletableFuture> = + future { + asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + + KoreaderSyncService.pushProgress(input.chapterId) + + val chapter = + transaction { + ChapterTable + .selectAll() + .where { ChapterTable.id eq input.chapterId } + .firstOrNull() + ?.let { ChapterType(it) } + } + + PushKoSyncProgressPayload( + clientMutationId = input.clientMutationId, + success = true, + chapter = chapter, + ) + } + } + + data class PullKoSyncProgressInput( + val clientMutationId: String? = null, + val chapterId: Int, + ) + + data class PullKoSyncProgressPayload( + val clientMutationId: String?, + val chapter: ChapterType?, + val syncConflict: SyncConflictInfoType?, + ) + + fun pullKoSyncProgress( + dataFetchingEnvironment: DataFetchingEnvironment, + input: PullKoSyncProgressInput, + ): CompletableFuture> = + future { + asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + + val syncResult = KoreaderSyncService.checkAndPullProgress(input.chapterId) + var syncConflictInfo: SyncConflictInfoType? = null + + if (syncResult != null) { + if (syncResult.isConflict) { + syncConflictInfo = + SyncConflictInfoType( + deviceName = syncResult.device, + remotePage = syncResult.pageRead, + ) + } + + if (syncResult.shouldUpdate) { + transaction { + ChapterTable.update({ ChapterTable.id eq input.chapterId }) { + it[lastPageRead] = syncResult.pageRead + it[lastReadAt] = syncResult.timestamp + } + } + } + } + + val chapter = + transaction { + ChapterTable + .selectAll() + .where { ChapterTable.id eq input.chapterId } + .firstOrNull() + ?.let { ChapterType(it) } + } + + PullKoSyncProgressPayload( + clientMutationId = input.clientMutationId, + chapter = chapter, + syncConflict = syncConflictInfo, + ) + } + } }