diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt index 36627e9db..b6bfb59e5 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt @@ -13,10 +13,13 @@ import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.MediaProfile import org.gotson.komga.domain.model.MediaUnsupportedException +import org.gotson.komga.domain.model.R2Progression import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING +import org.gotson.komga.domain.model.toR2Progression import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.MediaRepository +import org.gotson.komga.domain.persistence.ReadProgressRepository import org.gotson.komga.domain.persistence.SeriesMetadataRepository import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.domain.service.BookLifecycle @@ -24,6 +27,7 @@ import org.gotson.komga.infrastructure.image.ImageType import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault +import org.gotson.komga.interfaces.api.dto.MEDIATYPE_PROGRESSION_JSON_VALUE import org.gotson.komga.interfaces.api.persistence.BookDtoRepository import org.springframework.core.io.FileSystemResource import org.springframework.http.ContentDisposition @@ -36,7 +40,10 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.util.MimeTypeUtils import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.context.request.ServletWebRequest import org.springframework.web.server.ResponseStatusException @@ -61,6 +68,7 @@ class CommonBookController( private val bookAnalyzer: BookAnalyzer, private val contentRestrictionChecker: ContentRestrictionChecker, private val contentDetector: ContentDetector, + private val readProgressRepository: ReadProgressRepository, ) { fun getWebPubManifestInternal( principal: KomgaPrincipal, @@ -345,4 +353,48 @@ class CommonBookController( throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved") } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + @GetMapping( + value = [ + "api/v1/books/{bookId}/progression", + "opds/v2/books/{bookId}/progression", + ], + produces = [MEDIATYPE_PROGRESSION_JSON_VALUE], + ) + fun getProgression( + @AuthenticationPrincipal principal: KomgaPrincipal, + @PathVariable bookId: String, + ): ResponseEntity = + bookRepository.findByIdOrNull(bookId)?.let { book -> + contentRestrictionChecker.checkContentRestriction(principal.user, book) + + readProgressRepository.findByBookIdAndUserIdOrNull(bookId, principal.user.id)?.let { + ResponseEntity.ok(it.toR2Progression()) + } ?: ResponseEntity.noContent().build() + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + @PutMapping( + value = [ + "api/v1/books/{bookId}/progression", + "opds/v2/books/{bookId}/progression", + ], + ) + @ResponseStatus(HttpStatus.NO_CONTENT) + fun markProgression( + @AuthenticationPrincipal principal: KomgaPrincipal, + @PathVariable bookId: String, + @RequestBody progression: R2Progression, + ) { + bookRepository.findByIdOrNull(bookId)?.let { book -> + contentRestrictionChecker.checkContentRestriction(principal.user, book) + + try { + bookLifecycle.markProgression(book, principal.user, progression) + } catch (e: IllegalStateException) { + throw ResponseStatusException(HttpStatus.CONFLICT, e.message) + } catch (e: IllegalArgumentException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) + } + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/OpdsGenerator.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/OpdsGenerator.kt index 89bfeb275..d77832ed2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/OpdsGenerator.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/OpdsGenerator.kt @@ -7,6 +7,8 @@ import org.gotson.komga.infrastructure.image.ImageType import org.gotson.komga.interfaces.api.dto.MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE import org.gotson.komga.interfaces.api.dto.MEDIATYPE_OPDS_JSON_VALUE import org.gotson.komga.interfaces.api.dto.MEDIATYPE_OPDS_PUBLICATION_JSON +import org.gotson.komga.interfaces.api.dto.MEDIATYPE_PROGRESSION_JSON_VALUE +import org.gotson.komga.interfaces.api.dto.REL_PROGRESSION_API import org.gotson.komga.interfaces.api.dto.WPLinkDto import org.gotson.komga.interfaces.api.dto.WPPublicationDto import org.gotson.komga.interfaces.api.opds.v2.ROUTE_AUTH @@ -46,14 +48,27 @@ class OpdsGenerator( mapOf( "authenticate" to mapOf( - "href" to ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path(ROUTE_AUTH).toUriString(), + "href" to ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray()).path(ROUTE_AUTH).toUriString(), "type" to MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE, ), ) + override fun getExtraLinks(bookId: String): List { + return buildList { + add( + WPLinkDto( + type = MEDIATYPE_PROGRESSION_JSON_VALUE, + rel = REL_PROGRESSION_API, + href = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray()).path("books/$bookId/progression").toUriString(), + properties = getExtraLinkProperties(), + ), + ) + } + } + fun generateOpdsAuthDocument() = AuthenticationDocumentDto( - id = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path(ROUTE_AUTH).toUriString(), + id = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray()).path(ROUTE_AUTH).toUriString(), title = "Komga", description = "Enter your email and password to authenticate.", links = diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/WebPubGenerator.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/WebPubGenerator.kt index 0b54d88d0..160d4cbcd 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/WebPubGenerator.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/WebPubGenerator.kt @@ -240,6 +240,8 @@ class WebPubGenerator( protected fun getExtraLinkProperties(): Map> = emptyMap() + protected fun getExtraLinks(bookId: String): List = emptyList() + private fun BookDto.toWPLinkDtos(uriBuilder: UriComponentsBuilder): List { val komgaMediaType = KomgaMediaType.fromMediaType(media.mediaType) return buildList { @@ -250,6 +252,8 @@ class WebPubGenerator( add(WPLinkDto(href = uriBuilder.cloneBuilder().path("books/$id/manifest/divina").toUriString(), type = MEDIATYPE_DIVINA_JSON_VALUE, properties = getExtraLinkProperties())) // main acquisition link add(WPLinkDto(rel = OpdsLinkRel.ACQUISITION, type = komgaMediaType?.exportType ?: media.mediaType, href = uriBuilder.cloneBuilder().path("books/$id/file").toUriString(), properties = getExtraLinkProperties())) + // extra links + addAll(getExtraLinks(id)) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/dto/Constants.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/dto/Constants.kt index b6062e3b5..04c7ce3d3 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/dto/Constants.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/dto/Constants.kt @@ -14,6 +14,8 @@ const val PROFILE_DIVINA = "https://readium.org/webpub-manifest/profiles/divina" const val PROFILE_EPUB = "https://readium.org/webpub-manifest/profiles/epub" const val PROFILE_PDF = "https://readium.org/webpub-manifest/profiles/pdf" +const val REL_PROGRESSION_API = "http://www.cantook.com/api/progression" + val MEDIATYPE_OPDS_PUBLICATION_JSON = MediaType("application", "opds-publication+json") val MEDIATYPE_DIVINA_JSON = MediaType("application", "divina+json") val MEDIATYPE_WEBPUB_JSON = MediaType("application", "webpub+json") diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt index aee4b5a1f..53d368cb2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt @@ -21,12 +21,10 @@ import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaExtensionEpub import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.MediaProfile -import org.gotson.komga.domain.model.R2Progression import org.gotson.komga.domain.model.ROLE_ADMIN import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.domain.model.ThumbnailBook -import org.gotson.komga.domain.model.toR2Progression import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.MediaRepository @@ -48,7 +46,6 @@ import org.gotson.komga.interfaces.api.WebPubGenerator import org.gotson.komga.interfaces.api.dto.MEDIATYPE_DIVINA_JSON_VALUE import org.gotson.komga.interfaces.api.dto.MEDIATYPE_POSITION_LIST_JSON import org.gotson.komga.interfaces.api.dto.MEDIATYPE_POSITION_LIST_JSON_VALUE -import org.gotson.komga.interfaces.api.dto.MEDIATYPE_PROGRESSION_JSON_VALUE import org.gotson.komga.interfaces.api.dto.MEDIATYPE_WEBPUB_JSON_VALUE import org.gotson.komga.interfaces.api.dto.WPPublicationDto import org.gotson.komga.interfaces.api.getBookLastModified @@ -527,42 +524,6 @@ class BookController( .body(R2Positions(extension.positions.size, extension.positions)) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - @GetMapping( - value = ["api/v1/books/{bookId}/progression"], - produces = [MEDIATYPE_PROGRESSION_JSON_VALUE], - ) - fun getProgression( - @AuthenticationPrincipal principal: KomgaPrincipal, - @PathVariable bookId: String, - ): ResponseEntity = - bookRepository.findByIdOrNull(bookId)?.let { book -> - contentRestrictionChecker.checkContentRestriction(principal.user, book) - - readProgressRepository.findByBookIdAndUserIdOrNull(bookId, principal.user.id)?.let { - ResponseEntity.ok(it.toR2Progression()) - } ?: ResponseEntity.noContent().build() - } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - - @PutMapping("api/v1/books/{bookId}/progression") - @ResponseStatus(HttpStatus.NO_CONTENT) - fun markProgression( - @AuthenticationPrincipal principal: KomgaPrincipal, - @PathVariable bookId: String, - @RequestBody progression: R2Progression, - ) { - bookRepository.findByIdOrNull(bookId)?.let { book -> - contentRestrictionChecker.checkContentRestriction(principal.user, book) - - try { - bookLifecycle.markProgression(book, principal.user, progression) - } catch (e: IllegalStateException) { - throw ResponseStatusException(HttpStatus.CONFLICT, e.message) - } catch (e: IllegalArgumentException) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) - } - } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - } - @GetMapping( value = ["api/v1/books/{bookId}/manifest/epub"], produces = [MEDIATYPE_WEBPUB_JSON_VALUE],