refactor(kosync): introduce differentiated sync strategies (#1624)

* refactor(kosync): introduce differentiated sync strategies

Replaces the single `koreaderSyncStrategy` setting with `koreaderSyncStrategyForward` and `koreaderSyncStrategyBackward`. This allows users to define distinct conflict resolution behaviors based on whether the remote progress is newer or older than the local progress.

The `KoreaderSyncStrategy` enum has been simplified to `KoreaderSyncConflictStrategy` with four clear options: `PROMPT`, `KEEP_LOCAL`, `KEEP_REMOTE`, and `DISABLED`. The ambiguous `SILENT` option is removed, as its behavior is now implicitly covered by selecting `KEEP_REMOTE` for forward syncs and `KEEP_LOCAL` for backward syncs.

The legacy `koreaderSyncStrategy` setting is now deprecated and is seamlessly migrated to the new dual-strategy system using `MigratedConfigValue`, ensuring backward compatibility for existing user configurations.

* fix(kosync): correct proto numbers and setting order for sync strategies

* fix(kosync): proto number 78 to 68

* fix(server): migrate KOReader sync strategy during settings cleanup

Add migration logic to convert the old `server.koreaderSyncStrategy` key
into the new `server.koreaderSyncStrategyForward` and
`server.koreaderSyncStrategyBackward` keys during server setup.
This commit is contained in:
Zeedif
2025-09-09 16:12:53 -06:00
committed by GitHub
parent 5bf2a4aed4
commit 257e1dd03d
4 changed files with 143 additions and 24 deletions
@@ -16,7 +16,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload
import suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod
import suwayomi.tachidesk.graphql.types.KoreaderSyncStrategy
import suwayomi.tachidesk.graphql.types.KoreaderSyncConflictStrategy
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
import suwayomi.tachidesk.manga.impl.util.KoreaderHelper
import suwayomi.tachidesk.manga.model.table.ChapterTable
@@ -274,8 +274,15 @@ object KoreaderSyncService {
}
suspend fun pushProgress(chapterId: Int) {
val strategy = serverConfig.koreaderSyncStrategy.value
if (strategy == KoreaderSyncStrategy.DISABLED || strategy == KoreaderSyncStrategy.RECEIVE) return
val forwardStrategy = serverConfig.koreaderSyncStrategyForward.value
val backwardStrategy = serverConfig.koreaderSyncStrategyBackward.value
// if both directions keep remote, is in receive-only mode, so don't push.
if (forwardStrategy == KoreaderSyncConflictStrategy.KEEP_REMOTE &&
backwardStrategy == KoreaderSyncConflictStrategy.KEEP_REMOTE
) {
return
}
val username = serverConfig.koreaderSyncUsername.value
val userkey = serverConfig.koreaderSyncUserkey.value
@@ -346,8 +353,15 @@ object KoreaderSyncService {
}
suspend fun checkAndPullProgress(chapterId: Int): SyncResult? {
val strategy = serverConfig.koreaderSyncStrategy.value
if (strategy == KoreaderSyncStrategy.DISABLED || strategy == KoreaderSyncStrategy.SEND) return null
val forwardStrategy = serverConfig.koreaderSyncStrategyForward.value
val backwardStrategy = serverConfig.koreaderSyncStrategyBackward.value
// Skip remote fetch if both directions disabled OR both keep local (no remote data needed)
if ((forwardStrategy == KoreaderSyncConflictStrategy.DISABLED && backwardStrategy == KoreaderSyncConflictStrategy.DISABLED) ||
(forwardStrategy == KoreaderSyncConflictStrategy.KEEP_LOCAL && backwardStrategy == KoreaderSyncConflictStrategy.KEEP_LOCAL)
) {
return null
}
val username = serverConfig.koreaderSyncUsername.value
val userkey = serverConfig.koreaderSyncUserkey.value
@@ -417,19 +431,14 @@ object KoreaderSyncService {
return null
}
when (strategy) {
KoreaderSyncStrategy.RECEIVE -> {
return SyncResult(pageRead, timestamp, device, shouldUpdate = true)
}
KoreaderSyncStrategy.SILENT -> {
if (timestamp > (localProgress?.lastReadAt ?: 0L)) {
return SyncResult(pageRead, timestamp, device, shouldUpdate = true)
}
}
KoreaderSyncStrategy.PROMPT -> {
return SyncResult(pageRead, timestamp, device, isConflict = true)
}
else -> {} // SEND and DISABLED already handled at the start of the function
val localTimestamp = localProgress?.lastReadAt ?: 0L
val isRemoteNewer = timestamp > localTimestamp
val strategy = if (isRemoteNewer) forwardStrategy else backwardStrategy
return when (strategy) {
KoreaderSyncConflictStrategy.PROMPT -> SyncResult(pageRead, timestamp, device, isConflict = true)
KoreaderSyncConflictStrategy.KEEP_REMOTE -> SyncResult(pageRead, timestamp, device, shouldUpdate = true)
KoreaderSyncConflictStrategy.KEEP_LOCAL, KoreaderSyncConflictStrategy.DISABLED -> null
}
}
} else {
@@ -339,6 +339,31 @@ fun applicationSetup() {
"server.authPassword",
toType = { it.unwrapped() as? String },
)
// Migrate KOReader sync strategy from single to forward/backward strategies
try {
val oldStrategy = config.getString("server.koreaderSyncStrategy")
val (forward, backward) =
when (oldStrategy.uppercase()) {
"PROMPT" -> "PROMPT" to "PROMPT"
"SILENT" -> "KEEP_REMOTE" to "KEEP_LOCAL"
"SEND" -> "KEEP_LOCAL" to "KEEP_LOCAL"
"RECEIVE" -> "KEEP_REMOTE" to "KEEP_REMOTE"
"DISABLED" -> "DISABLED" to "DISABLED"
else -> null to null
}
if (forward != null && backward != null) {
updatedConfig =
updatedConfig
.withValue("server.koreaderSyncStrategyForward", forward.toConfig("internal").getValue("internal"))
.withValue("server.koreaderSyncStrategyBackward", backward.toConfig("internal").getValue("internal"))
.withoutPath("server.koreaderSyncStrategy")
}
} catch (_: ConfigException.Missing) {
// Key doesn't exist, no migration needed
}
updatedConfig
}
}