Skip to content

Commit fc56b12

Browse files
committed
kobo sync merge store results
1 parent e72de01 commit fc56b12

File tree

3 files changed

+47
-22
lines changed

3 files changed

+47
-22
lines changed

komga/src/main/kotlin/org/gotson/komga/infrastructure/kobo/KoboProxy.kt

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package org.gotson.komga.infrastructure.kobo
33
import com.fasterxml.jackson.databind.JsonNode
44
import com.fasterxml.jackson.databind.ObjectMapper
55
import io.github.oshai.kotlinlogging.KotlinLogging
6-
import org.gotson.komga.domain.model.KomgaSyncToken
76
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
87
import org.gotson.komga.infrastructure.kobo.KoboHeaders.X_KOBO_SYNCTOKEN
98
import org.gotson.komga.infrastructure.web.getCurrentRequest
@@ -60,6 +59,12 @@ class KoboProxy(
6059

6160
fun isEnabled() = komgaSettingsProvider.koboProxy
6261

62+
/**
63+
* Proxy the current request to the Kobo store, if enabled.
64+
* If [includeSyncToken] is set, the raw sync token will be extracted from the current request and sent to the store.
65+
* If a X_KOBO_SYNCTOKEN header is present in the response, the original Komga sync token will be updated with the
66+
* raw Kobo sync token returned, and added to the response headers.
67+
*/
6368
fun proxyCurrentRequest(
6469
body: Any? = null,
6570
includeSyncToken: Boolean = false,
@@ -115,8 +120,8 @@ class KoboProxy(
115120
.apply {
116121
if (keys.contains(X_KOBO_SYNCTOKEN, true)) {
117122
val koboSyncToken = this[X_KOBO_SYNCTOKEN]?.firstOrNull()
118-
if (koboSyncToken != null) {
119-
val komgaSyncToken = syncToken?.copy(rawKoboSyncToken = koboSyncToken) ?: KomgaSyncToken(rawKoboSyncToken = koboSyncToken)
123+
if (koboSyncToken != null && includeSyncToken && syncToken != null) {
124+
val komgaSyncToken = syncToken.copy(rawKoboSyncToken = koboSyncToken)
120125
this[X_KOBO_SYNCTOKEN] = listOf(komgaSyncTokenGenerator.toBase64(komgaSyncToken))
121126
}
122127
}

komga/src/main/kotlin/org/gotson/komga/interfaces/api/kobo/KoboController.kt

+30-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.gotson.komga.interfaces.api.kobo
33
import com.fasterxml.jackson.databind.JsonNode
44
import com.fasterxml.jackson.databind.ObjectMapper
55
import com.fasterxml.jackson.databind.node.ObjectNode
6+
import com.fasterxml.jackson.module.kotlin.treeToValue
67
import io.github.oshai.kotlinlogging.KotlinLogging
78
import org.apache.commons.lang3.RandomStringUtils
89
import org.gotson.komga.domain.model.KomgaSyncToken
@@ -175,20 +176,20 @@ class KoboController(
175176
@AuthenticationPrincipal principal: KomgaPrincipal,
176177
@PathVariable authToken: String,
177178
): ResponseEntity<Collection<SyncResultDto>> {
178-
val syncToken = komgaSyncTokenGenerator.fromRequestHeaders(getCurrentRequest()) ?: KomgaSyncToken()
179+
val syncTokenReceived = komgaSyncTokenGenerator.fromRequestHeaders(getCurrentRequest()) ?: KomgaSyncToken()
179180

180181
// find the ongoing sync point, else create one
181182
val toSyncPoint =
182-
getSyncPointVerified(syncToken.ongoingSyncPointId, principal.user.id)
183-
?: syncPointLifecycle.createSyncPoint(principal.user, null) // TODO: for now we sync all libraries
183+
getSyncPointVerified(syncTokenReceived.ongoingSyncPointId, principal.user.id)
184+
?: syncPointLifecycle.createSyncPoint(principal.user, null) // for now we sync all libraries
184185

185186
// find the last successful sync, if any
186-
val fromSyncPoint = getSyncPointVerified(syncToken.lastSuccessfulSyncPointId, principal.user.id)
187+
val fromSyncPoint = getSyncPointVerified(syncTokenReceived.lastSuccessfulSyncPointId, principal.user.id)
187188

188189
logger.debug { "Library sync from SyncPoint $fromSyncPoint, to SyncPoint: $toSyncPoint" }
189190

190191
var shouldContinueSync: Boolean
191-
val syncResult: Collection<SyncResultDto> =
192+
val syncResultKomga: Collection<SyncResultDto> =
192193
if (fromSyncPoint != null) {
193194
// find books added/changed/removed and map to DTO
194195
var maxRemainingCount = komgaProperties.kobo.syncItemLimit
@@ -272,27 +273,42 @@ class KoboController(
272273
}
273274
}
274275

275-
// update synctoken to send back to Kobo
276+
// merge Kobo store sync response
277+
val (syncResultMerged, syncTokenMerged, shouldContinueSyncMerged) =
278+
if (koboProxy.isEnabled()) {
279+
try {
280+
val koboStoreResponse = koboProxy.proxyCurrentRequest(includeSyncToken = true)
281+
val syncResultsKobo = koboStoreResponse.body?.let { mapper.treeToValue<Collection<SyncResultDto>>(it) } ?: emptyList()
282+
val syncTokenKobo = koboStoreResponse.headers[X_KOBO_SYNCTOKEN]?.firstOrNull()?.let { komgaSyncTokenGenerator.fromBase64(it) }
283+
val shouldContinueSyncKobo = koboStoreResponse.headers[X_KOBO_SYNC]?.firstOrNull()?.lowercase() == "continue"
284+
285+
Triple(syncResultKomga + syncResultsKobo, syncTokenKobo ?: syncTokenReceived, shouldContinueSyncKobo || shouldContinueSync)
286+
} catch (e: Exception) {
287+
logger.error(e) { "Kobo sync endpoint failure" }
288+
Triple(syncResultKomga, syncTokenReceived, shouldContinueSync)
289+
}
290+
} else {
291+
Triple(syncResultKomga, syncTokenReceived, shouldContinueSync)
292+
}
293+
294+
// update synctoken to send back to Kobo device
276295
val syncTokenUpdated =
277-
if (shouldContinueSync) {
278-
syncToken.copy(ongoingSyncPointId = toSyncPoint.id)
296+
if (shouldContinueSyncMerged) {
297+
syncTokenMerged.copy(ongoingSyncPointId = toSyncPoint.id)
279298
} else {
280299
// cleanup old syncpoint if it exists
281300
fromSyncPoint?.let { syncPointRepository.deleteOne(it.id) }
282301

283-
syncToken.copy(ongoingSyncPointId = null, lastSuccessfulSyncPointId = toSyncPoint.id)
302+
syncTokenMerged.copy(ongoingSyncPointId = null, lastSuccessfulSyncPointId = toSyncPoint.id)
284303
}
285304

286-
// TODO: merge kobo store response
287-
// return koboProxy.proxyCurrentRequest(includeSyncToken = true)
288-
289305
return ResponseEntity
290306
.ok()
291307
.headers {
292-
if (shouldContinueSync) it.set(X_KOBO_SYNC, "continue")
308+
if (shouldContinueSyncMerged) it.set(X_KOBO_SYNC, "continue")
293309
it.set(X_KOBO_SYNCTOKEN, komgaSyncTokenGenerator.toBase64(syncTokenUpdated))
294310
}
295-
.body(syncResult)
311+
.body(syncResultMerged)
296312
}
297313

298314
@GetMapping("/v1/library/{bookId}/metadata")

komga/src/test/kotlin/org/gotson/komga/interfaces/api/kobo/KoboControllerTest.kt

+9-5
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,14 @@ class KoboControllerTest(
118118
fun `given kobo proxy is enabled when requesting book cover for non-existent book then redirect response is returned`() {
119119
komgaSettingsProvider.koboProxy = true
120120

121-
mockMvc.get("/kobo/$apiKey/v1/books/nonexistent/thumbnail/800/800/false/image.jpg")
122-
.andExpect {
123-
status { isTemporaryRedirect() }
124-
header { string(HttpHeaders.LOCATION, "https://cdn.kobo.com/book-images/nonexistent/800/800/false/image.jpg") }
125-
}
121+
try {
122+
mockMvc.get("/kobo/$apiKey/v1/books/nonexistent/thumbnail/800/800/false/image.jpg")
123+
.andExpect {
124+
status { isTemporaryRedirect() }
125+
header { string(HttpHeaders.LOCATION, "https://cdn.kobo.com/book-images/nonexistent/800/800/false/image.jpg") }
126+
}
127+
} finally {
128+
komgaSettingsProvider.koboProxy = false
129+
}
126130
}
127131
}

0 commit comments

Comments
 (0)