mirror of
https://github.com/gotson/komga.git
synced 2026-01-07 00:13:24 +01:00
feat(opds): discoverable Progression API, used by Aldiko/Cantook
Refs: https://github.com/opds-community/drafts/discussions/67#discussioncomment-6414507
This commit is contained in:
parent
14bee1732a
commit
443d8a70b4
5 changed files with 75 additions and 41 deletions
|
|
@ -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<R2Progression> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WPLinkDto> {
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -240,6 +240,8 @@ class WebPubGenerator(
|
|||
|
||||
protected fun getExtraLinkProperties(): Map<String, Map<String, Any>> = emptyMap()
|
||||
|
||||
protected fun getExtraLinks(bookId: String): List<WPLinkDto> = emptyList()
|
||||
|
||||
private fun BookDto.toWPLinkDtos(uriBuilder: UriComponentsBuilder): List<WPLinkDto> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<R2Progression> =
|
||||
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],
|
||||
|
|
|
|||
Loading…
Reference in a new issue