Skip to content

Commit

Permalink
kobo sync merge store results
Browse files Browse the repository at this point in the history
  • Loading branch information
gotson committed Aug 16, 2024
1 parent e72de01 commit fc56b12
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package org.gotson.komga.infrastructure.kobo
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gotson.komga.domain.model.KomgaSyncToken
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.kobo.KoboHeaders.X_KOBO_SYNCTOKEN
import org.gotson.komga.infrastructure.web.getCurrentRequest
Expand Down Expand Up @@ -60,6 +59,12 @@ class KoboProxy(

fun isEnabled() = komgaSettingsProvider.koboProxy

/**
* Proxy the current request to the Kobo store, if enabled.
* If [includeSyncToken] is set, the raw sync token will be extracted from the current request and sent to the store.
* If a X_KOBO_SYNCTOKEN header is present in the response, the original Komga sync token will be updated with the
* raw Kobo sync token returned, and added to the response headers.
*/
fun proxyCurrentRequest(
body: Any? = null,
includeSyncToken: Boolean = false,
Expand Down Expand Up @@ -115,8 +120,8 @@ class KoboProxy(
.apply {
if (keys.contains(X_KOBO_SYNCTOKEN, true)) {
val koboSyncToken = this[X_KOBO_SYNCTOKEN]?.firstOrNull()
if (koboSyncToken != null) {
val komgaSyncToken = syncToken?.copy(rawKoboSyncToken = koboSyncToken) ?: KomgaSyncToken(rawKoboSyncToken = koboSyncToken)
if (koboSyncToken != null && includeSyncToken && syncToken != null) {
val komgaSyncToken = syncToken.copy(rawKoboSyncToken = koboSyncToken)
this[X_KOBO_SYNCTOKEN] = listOf(komgaSyncTokenGenerator.toBase64(komgaSyncToken))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.gotson.komga.interfaces.api.kobo
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.treeToValue
import io.github.oshai.kotlinlogging.KotlinLogging
import org.apache.commons.lang3.RandomStringUtils
import org.gotson.komga.domain.model.KomgaSyncToken
Expand Down Expand Up @@ -175,20 +176,20 @@ class KoboController(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable authToken: String,
): ResponseEntity<Collection<SyncResultDto>> {
val syncToken = komgaSyncTokenGenerator.fromRequestHeaders(getCurrentRequest()) ?: KomgaSyncToken()
val syncTokenReceived = komgaSyncTokenGenerator.fromRequestHeaders(getCurrentRequest()) ?: KomgaSyncToken()

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

// find the last successful sync, if any
val fromSyncPoint = getSyncPointVerified(syncToken.lastSuccessfulSyncPointId, principal.user.id)
val fromSyncPoint = getSyncPointVerified(syncTokenReceived.lastSuccessfulSyncPointId, principal.user.id)

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

var shouldContinueSync: Boolean
val syncResult: Collection<SyncResultDto> =
val syncResultKomga: Collection<SyncResultDto> =
if (fromSyncPoint != null) {
// find books added/changed/removed and map to DTO
var maxRemainingCount = komgaProperties.kobo.syncItemLimit
Expand Down Expand Up @@ -272,27 +273,42 @@ class KoboController(
}
}

// update synctoken to send back to Kobo
// merge Kobo store sync response
val (syncResultMerged, syncTokenMerged, shouldContinueSyncMerged) =
if (koboProxy.isEnabled()) {
try {
val koboStoreResponse = koboProxy.proxyCurrentRequest(includeSyncToken = true)
val syncResultsKobo = koboStoreResponse.body?.let { mapper.treeToValue<Collection<SyncResultDto>>(it) } ?: emptyList()
val syncTokenKobo = koboStoreResponse.headers[X_KOBO_SYNCTOKEN]?.firstOrNull()?.let { komgaSyncTokenGenerator.fromBase64(it) }
val shouldContinueSyncKobo = koboStoreResponse.headers[X_KOBO_SYNC]?.firstOrNull()?.lowercase() == "continue"

Triple(syncResultKomga + syncResultsKobo, syncTokenKobo ?: syncTokenReceived, shouldContinueSyncKobo || shouldContinueSync)
} catch (e: Exception) {
logger.error(e) { "Kobo sync endpoint failure" }
Triple(syncResultKomga, syncTokenReceived, shouldContinueSync)
}
} else {
Triple(syncResultKomga, syncTokenReceived, shouldContinueSync)
}

// update synctoken to send back to Kobo device
val syncTokenUpdated =
if (shouldContinueSync) {
syncToken.copy(ongoingSyncPointId = toSyncPoint.id)
if (shouldContinueSyncMerged) {
syncTokenMerged.copy(ongoingSyncPointId = toSyncPoint.id)
} else {
// cleanup old syncpoint if it exists
fromSyncPoint?.let { syncPointRepository.deleteOne(it.id) }

syncToken.copy(ongoingSyncPointId = null, lastSuccessfulSyncPointId = toSyncPoint.id)
syncTokenMerged.copy(ongoingSyncPointId = null, lastSuccessfulSyncPointId = toSyncPoint.id)
}

// TODO: merge kobo store response
// return koboProxy.proxyCurrentRequest(includeSyncToken = true)

return ResponseEntity
.ok()
.headers {
if (shouldContinueSync) it.set(X_KOBO_SYNC, "continue")
if (shouldContinueSyncMerged) it.set(X_KOBO_SYNC, "continue")
it.set(X_KOBO_SYNCTOKEN, komgaSyncTokenGenerator.toBase64(syncTokenUpdated))
}
.body(syncResult)
.body(syncResultMerged)
}

@GetMapping("/v1/library/{bookId}/metadata")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,14 @@ class KoboControllerTest(
fun `given kobo proxy is enabled when requesting book cover for non-existent book then redirect response is returned`() {
komgaSettingsProvider.koboProxy = true

mockMvc.get("/kobo/$apiKey/v1/books/nonexistent/thumbnail/800/800/false/image.jpg")
.andExpect {
status { isTemporaryRedirect() }
header { string(HttpHeaders.LOCATION, "https://cdn.kobo.com/book-images/nonexistent/800/800/false/image.jpg") }
}
try {
mockMvc.get("/kobo/$apiKey/v1/books/nonexistent/thumbnail/800/800/false/image.jpg")
.andExpect {
status { isTemporaryRedirect() }
header { string(HttpHeaders.LOCATION, "https://cdn.kobo.com/book-images/nonexistent/800/800/false/image.jpg") }
}
} finally {
komgaSettingsProvider.koboProxy = false
}
}
}

0 comments on commit fc56b12

Please sign in to comment.