@@ -6,14 +6,20 @@ import com.fasterxml.jackson.databind.node.ObjectNode
6
6
import com.fasterxml.jackson.module.kotlin.treeToValue
7
7
import io.github.oshai.kotlinlogging.KotlinLogging
8
8
import org.apache.commons.lang3.RandomStringUtils
9
+ import org.gotson.komga.domain.model.Book
9
10
import org.gotson.komga.domain.model.KomgaSyncToken
11
+ import org.gotson.komga.domain.model.R2Device
12
+ import org.gotson.komga.domain.model.R2Locator
13
+ import org.gotson.komga.domain.model.R2Progression
10
14
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
11
15
import org.gotson.komga.domain.model.SyncPoint
12
16
import org.gotson.komga.domain.persistence.BookRepository
17
+ import org.gotson.komga.domain.persistence.ReadProgressRepository
13
18
import org.gotson.komga.domain.persistence.SyncPointRepository
14
19
import org.gotson.komga.domain.service.BookLifecycle
15
20
import org.gotson.komga.domain.service.SyncPointLifecycle
16
21
import org.gotson.komga.infrastructure.configuration.KomgaProperties
22
+ import org.gotson.komga.infrastructure.kobo.KoboHeaders.X_KOBO_DEVICEID
17
23
import org.gotson.komga.infrastructure.kobo.KoboHeaders.X_KOBO_SYNC
18
24
import org.gotson.komga.infrastructure.kobo.KoboHeaders.X_KOBO_SYNCTOKEN
19
25
import org.gotson.komga.infrastructure.kobo.KoboHeaders.X_KOBO_USERKEY
@@ -24,14 +30,24 @@ import org.gotson.komga.infrastructure.web.getCurrentRequest
24
30
import org.gotson.komga.interfaces.api.CommonBookController
25
31
import org.gotson.komga.interfaces.api.kobo.dto.AuthDto
26
32
import org.gotson.komga.interfaces.api.kobo.dto.BookEntitlementContainerDto
33
+ import org.gotson.komga.interfaces.api.kobo.dto.BookmarkDto
27
34
import org.gotson.komga.interfaces.api.kobo.dto.ChangedEntitlementDto
28
35
import org.gotson.komga.interfaces.api.kobo.dto.KoboBookMetadataDto
29
36
import org.gotson.komga.interfaces.api.kobo.dto.NewEntitlementDto
37
+ import org.gotson.komga.interfaces.api.kobo.dto.ReadingStateDto
38
+ import org.gotson.komga.interfaces.api.kobo.dto.ReadingStateUpdateResultDto
39
+ import org.gotson.komga.interfaces.api.kobo.dto.RequestResultDto
30
40
import org.gotson.komga.interfaces.api.kobo.dto.ResourcesDto
41
+ import org.gotson.komga.interfaces.api.kobo.dto.ResultDto
42
+ import org.gotson.komga.interfaces.api.kobo.dto.StatisticsDto
43
+ import org.gotson.komga.interfaces.api.kobo.dto.StatusDto
44
+ import org.gotson.komga.interfaces.api.kobo.dto.StatusInfoDto
31
45
import org.gotson.komga.interfaces.api.kobo.dto.SyncResultDto
32
46
import org.gotson.komga.interfaces.api.kobo.dto.TestsDto
33
47
import org.gotson.komga.interfaces.api.kobo.dto.toBookEntitlementDto
48
+ import org.gotson.komga.interfaces.api.kobo.dto.toDto
34
49
import org.gotson.komga.interfaces.api.kobo.persistence.KoboDtoRepository
50
+ import org.gotson.komga.language.toUTCZoned
35
51
import org.springframework.data.domain.Page
36
52
import org.springframework.data.domain.Pageable
37
53
import org.springframework.http.HttpStatus
@@ -42,6 +58,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
42
58
import org.springframework.web.bind.annotation.GetMapping
43
59
import org.springframework.web.bind.annotation.PathVariable
44
60
import org.springframework.web.bind.annotation.PostMapping
61
+ import org.springframework.web.bind.annotation.PutMapping
45
62
import org.springframework.web.bind.annotation.RequestBody
46
63
import org.springframework.web.bind.annotation.RequestHeader
47
64
import org.springframework.web.bind.annotation.RequestMapping
@@ -52,6 +69,7 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
52
69
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
53
70
import org.springframework.web.util.UriBuilder
54
71
import org.springframework.web.util.UriComponentsBuilder
72
+ import java.time.ZonedDateTime
55
73
import java.util.UUID
56
74
57
75
private val logger = KotlinLogging .logger {}
@@ -116,6 +134,7 @@ class KoboController(
116
134
private val commonBookController : CommonBookController ,
117
135
private val bookLifecycle : BookLifecycle ,
118
136
private val bookRepository : BookRepository ,
137
+ private val readProgressRepository : ReadProgressRepository ,
119
138
) {
120
139
@GetMapping(" ping" )
121
140
fun ping () = " pong"
@@ -143,6 +162,9 @@ class KoboController(
143
162
.body(ResourcesDto (resources))
144
163
}
145
164
165
+ /* *
166
+ * @return an [AuthDto]
167
+ */
146
168
@PostMapping(" v1/auth/device" )
147
169
fun authDevice (
148
170
@RequestBody body : JsonNode ,
@@ -173,6 +195,9 @@ class KoboController(
173
195
testKey = userKey ? : " " ,
174
196
)
175
197
198
+ /* *
199
+ * @return an array of [SyncResultDto]
200
+ */
176
201
@GetMapping(" v1/library/sync" )
177
202
fun syncLibrary (
178
203
@AuthenticationPrincipal principal : KomgaPrincipal ,
@@ -222,7 +247,8 @@ class KoboController(
222
247
223
248
logger.debug { " Library sync: ${booksAdded.numberOfElements} books added, ${booksChanged.numberOfElements} books changed, ${booksRemoved.numberOfElements} books removed" }
224
249
225
- val metadata = koboDtoRepository.findBookMetadataByIds((booksAdded.content + booksChanged.content + booksRemoved.content).map { it.bookId }, getDownloadUrlBuilder(authToken)).associateBy { it.entitlementId }
250
+ val metadata = koboDtoRepository.findBookMetadataByIds((booksAdded.content + booksChanged.content).map { it.bookId }, getDownloadUrlBuilder(authToken)).associateBy { it.entitlementId }
251
+ val readProgress = readProgressRepository.findAllByBookIdsAndUserId((booksAdded.content + booksChanged.content).map { it.bookId }, principal.user.id).associateBy { it.bookId }
226
252
227
253
buildList {
228
254
addAll(
@@ -231,6 +257,7 @@ class KoboController(
231
257
BookEntitlementContainerDto (
232
258
bookEntitlement = it.toBookEntitlementDto(false ),
233
259
bookMetadata = metadata[it.bookId]!! ,
260
+ readingState = readProgress[it.bookId]?.toDto() ? : getEmptyReadProgressForBook(it.bookId, it.createdDate),
234
261
),
235
262
)
236
263
},
@@ -241,6 +268,7 @@ class KoboController(
241
268
BookEntitlementContainerDto (
242
269
bookEntitlement = it.toBookEntitlementDto(false ),
243
270
bookMetadata = metadata[it.bookId]!! ,
271
+ readingState = readProgress[it.bookId]?.toDto() ? : getEmptyReadProgressForBook(it.bookId, it.createdDate),
244
272
),
245
273
)
246
274
},
@@ -264,12 +292,14 @@ class KoboController(
264
292
logger.debug { " Library sync: ${books.numberOfElements} books" }
265
293
266
294
val metadata = koboDtoRepository.findBookMetadataByIds(books.content.map { it.bookId }, getDownloadUrlBuilder(authToken)).associateBy { it.entitlementId }
295
+ val readProgress = readProgressRepository.findAllByBookIdsAndUserId(books.content.map { it.bookId }, principal.user.id).associateBy { it.bookId }
267
296
268
297
books.content.map {
269
298
NewEntitlementDto (
270
299
BookEntitlementContainerDto (
271
300
bookEntitlement = it.toBookEntitlementDto(false ),
272
301
bookMetadata = metadata[it.bookId]!! ,
302
+ readingState = readProgress[it.bookId]?.toDto() ? : getEmptyReadProgressForBook(it.bookId, it.createdDate),
273
303
),
274
304
)
275
305
}
@@ -313,6 +343,9 @@ class KoboController(
313
343
.body(syncResultMerged)
314
344
}
315
345
346
+ /* *
347
+ * @return an array of [KoboBookMetadataDto]
348
+ */
316
349
@GetMapping(" /v1/library/{bookId}/metadata" )
317
350
fun getBookMetadata (
318
351
@PathVariable authToken : String ,
@@ -323,6 +356,100 @@ class KoboController(
323
356
else
324
357
ResponseEntity .ok(koboDtoRepository.findBookMetadataByIds(listOf (bookId), getDownloadUrlBuilder(authToken)))
325
358
359
+ /* *
360
+ * @return an array of [ReadingStateDto]
361
+ */
362
+ @GetMapping(" /v1/library/{bookId}/state" )
363
+ fun getState (
364
+ @AuthenticationPrincipal principal : KomgaPrincipal ,
365
+ @PathVariable bookId : String ,
366
+ ): ResponseEntity <* > {
367
+ val book =
368
+ bookRepository.findByIdOrNull(bookId)
369
+ ? : if (koboProxy.isEnabled())
370
+ return koboProxy.proxyCurrentRequest()
371
+ else
372
+ throw ResponseStatusException (HttpStatus .NOT_FOUND )
373
+
374
+ val response = readProgressRepository.findByBookIdAndUserIdOrNull(bookId, principal.user.id)?.toDto() ? : getEmptyReadProgressForBook(book)
375
+ return ResponseEntity .ok(listOf (response))
376
+ }
377
+
378
+ /* *
379
+ * @return a [RequestResultDto]
380
+ */
381
+ @PutMapping(" /v1/library/{bookId}/state" )
382
+ fun updateState (
383
+ @AuthenticationPrincipal principal : KomgaPrincipal ,
384
+ @PathVariable bookId : String ,
385
+ @RequestBody koboUpdate : ReadingStateDto ,
386
+ @RequestHeader(name = X_KOBO_DEVICEID , required = false ) koboDeviceId : String = "unknown",
387
+ ): ResponseEntity <* > {
388
+ val book =
389
+ bookRepository.findByIdOrNull(bookId)
390
+ ? : if (koboProxy.isEnabled())
391
+ return koboProxy.proxyCurrentRequest(koboUpdate)
392
+ else
393
+ throw ResponseStatusException (HttpStatus .NOT_FOUND )
394
+
395
+ if (koboUpdate.currentBookmark.location == null ) throw ResponseStatusException (HttpStatus .BAD_REQUEST )
396
+
397
+ // convert the Kobo update request to an R2Progression
398
+ val r2Progression =
399
+ R2Progression (
400
+ modified = koboUpdate.lastModified,
401
+ device =
402
+ R2Device (
403
+ id = koboDeviceId,
404
+ // TODO: get API key comment
405
+ name = " need to get the API key comment" ,
406
+ ),
407
+ locator =
408
+ R2Locator (
409
+ href = koboUpdate.currentBookmark.location.source,
410
+ // assume default
411
+ type = " application/xhtml+xml" ,
412
+ locations =
413
+ R2Locator .Location (
414
+ progression = koboUpdate.currentBookmark.progressPercent,
415
+ ),
416
+ ),
417
+ )
418
+
419
+ val response =
420
+ try {
421
+ bookLifecycle.markProgression(book, principal.user, r2Progression)
422
+
423
+ RequestResultDto (
424
+ requestResult = ResultDto .SUCCESS ,
425
+ updateResults =
426
+ listOf (
427
+ ReadingStateUpdateResultDto (
428
+ entitlementId = bookId,
429
+ currentBookmarkResult = ResultDto .SUCCESS .wrapped(),
430
+ statisticsResult = ResultDto .IGNORED .wrapped(),
431
+ statusInfoResult = ResultDto .SUCCESS .wrapped(),
432
+ ),
433
+ ),
434
+ )
435
+ } catch (e: Exception ) {
436
+ RequestResultDto (
437
+ requestResult = ResultDto .FAILURE ,
438
+ updateResults =
439
+ listOf (
440
+ ReadingStateUpdateResultDto (
441
+ entitlementId = bookId,
442
+ currentBookmarkResult = ResultDto .FAILURE .wrapped(),
443
+ statisticsResult = ResultDto .FAILURE .wrapped(),
444
+ statusInfoResult = ResultDto .FAILURE .wrapped(),
445
+ ),
446
+ ),
447
+ )
448
+ }
449
+
450
+ return ResponseEntity .ok(response)
451
+ }
452
+
326
453
@GetMapping(
327
454
value = [" v1/books/{bookId}/file/epub" ],
328
455
produces = [MediaType .APPLICATION_OCTET_STREAM_VALUE ],
@@ -396,4 +523,42 @@ class KoboController(
396
523
workId = bookId,
397
524
title = bookId,
398
525
)
526
+
527
+ private fun getEmptyReadProgressForBook (book : Book ): ReadingStateDto {
528
+ val createdDateUTC = book.createdDate.toUTCZoned()
529
+ return ReadingStateDto (
530
+ created = createdDateUTC,
531
+ lastModified = createdDateUTC,
532
+ priorityTimestamp = createdDateUTC,
533
+ entitlementId = book.id,
534
+ currentBookmark = BookmarkDto (createdDateUTC),
535
+ statistics = StatisticsDto (createdDateUTC),
536
+ statusInfo =
537
+ StatusInfoDto (
538
+ lastModified = createdDateUTC,
539
+ status = StatusDto .READY_TO_READ ,
540
+ timesStartedReading = 0 ,
541
+ ),
542
+ )
543
+ }
544
+
545
+ private fun getEmptyReadProgressForBook (
546
+ bookId : String ,
547
+ createdDate : ZonedDateTime ,
548
+ ): ReadingStateDto {
549
+ return ReadingStateDto (
550
+ created = createdDate,
551
+ lastModified = createdDate,
552
+ priorityTimestamp = createdDate,
553
+ entitlementId = bookId,
554
+ currentBookmark = BookmarkDto (createdDate),
555
+ statistics = StatisticsDto (createdDate),
556
+ statusInfo =
557
+ StatusInfoDto (
558
+ lastModified = createdDate,
559
+ status = StatusDto .READY_TO_READ ,
560
+ timesStartedReading = 0 ,
561
+ ),
562
+ )
563
+ }
399
564
}
0 commit comments