mirror of
https://github.com/gotson/komga.git
synced 2026-04-30 02:42:26 +02:00
feat(opds): support authentication for OPDS
always use /opds/v2 for webpub manifests served in OPDS 2 feeds
This commit is contained in:
parent
89a0f4ae44
commit
3250c123bd
9 changed files with 410 additions and 165 deletions
|
|
@ -0,0 +1,44 @@
|
|||
package org.gotson.komga.infrastructure.security
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.gotson.komga.interfaces.api.OpdsGenerator
|
||||
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE
|
||||
import org.gotson.komga.interfaces.api.dto.OpdsLinkRel
|
||||
import org.gotson.komga.interfaces.api.opds.v2.ROUTE_AUTH
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
|
||||
|
||||
private const val DEFAULT_REALM: String = "Realm"
|
||||
|
||||
@Component
|
||||
class OpdsAuthenticationEntryPoint(
|
||||
private val opdsGenerator: OpdsGenerator,
|
||||
private val objectMapper: ObjectMapper,
|
||||
) : AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException,
|
||||
) {
|
||||
with(response) {
|
||||
contentType = MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE
|
||||
characterEncoding = Charsets.UTF_8.name()
|
||||
status = HttpStatus.UNAUTHORIZED.value()
|
||||
setHeader("WWW-Authenticate", """Basic realm="$DEFAULT_REALM"""")
|
||||
setHeader(
|
||||
HttpHeaders.LINK,
|
||||
"""<${ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path(ROUTE_AUTH).toUriString()}>; rel="${OpdsLinkRel.AUTH}"; type="$MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE"""",
|
||||
)
|
||||
with(writer) {
|
||||
write(objectMapper.writeValueAsString(opdsGenerator.generateOpdsAuthDocument()))
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import org.springframework.security.web.SecurityFilterChain
|
|||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
||||
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ class SecurityConfiguration(
|
|||
private val sessionCookieName: String,
|
||||
private val userAgentWebAuthenticationDetailsSource: WebAuthenticationDetailsSource,
|
||||
private val sessionRegistry: SessionRegistry,
|
||||
private val opdsAuthenticationEntryPoint: OpdsAuthenticationEntryPoint,
|
||||
clientRegistrationRepository: InMemoryClientRegistrationRepository?,
|
||||
) {
|
||||
private val oauth2Enabled = clientRegistrationRepository != null
|
||||
|
|
@ -75,6 +77,8 @@ class SecurityConfiguration(
|
|||
"/api/v1/oauth2/providers",
|
||||
// epub resources - fonts are always requested anonymously, so we check for authorization within the controller method directly
|
||||
"api/v1/books/{bookId}/resource/**",
|
||||
// OPDS authentication document
|
||||
"/opds/v2/auth",
|
||||
).permitAll()
|
||||
|
||||
// all other endpoints are restricted to authenticated users
|
||||
|
|
@ -103,6 +107,9 @@ class SecurityConfiguration(
|
|||
it.maximumSessions(-1)
|
||||
}
|
||||
}
|
||||
.exceptionHandling {
|
||||
it.defaultAuthenticationEntryPointFor(opdsAuthenticationEntryPoint, AntPathRequestMatcher("/opds/v2/**"))
|
||||
}
|
||||
|
||||
if (oauth2Enabled) {
|
||||
http.oauth2Login { oauth2 ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package org.gotson.komga.interfaces.api
|
||||
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.service.BookAnalyzer
|
||||
import org.gotson.komga.infrastructure.image.ImageConverter
|
||||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
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.WPLinkDto
|
||||
import org.gotson.komga.interfaces.api.dto.WPPublicationDto
|
||||
import org.gotson.komga.interfaces.api.opds.v2.ROUTE_AUTH
|
||||
import org.gotson.komga.interfaces.api.opds.v2.dto.AuthenticationDocumentDto
|
||||
import org.gotson.komga.interfaces.api.opds.v2.dto.AuthenticationFlowDto
|
||||
import org.gotson.komga.interfaces.api.opds.v2.dto.AuthenticationType
|
||||
import org.gotson.komga.interfaces.api.opds.v2.dto.LabelsDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.BookDto
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
|
||||
|
||||
@Component
|
||||
class OpdsGenerator(
|
||||
@Qualifier("thumbnailType") thumbnailType: ImageType,
|
||||
imageConverter: ImageConverter,
|
||||
bookAnalyzer: BookAnalyzer,
|
||||
mediaRepository: MediaRepository,
|
||||
) : WebPubGenerator(thumbnailType, imageConverter, bookAnalyzer, mediaRepository, listOf("opds", "v2")) {
|
||||
fun toOpdsPublicationDto(bookDto: BookDto): WPPublicationDto =
|
||||
toBasePublicationDto(bookDto).copy(images = buildThumbnailLinkDtos(bookDto.id))
|
||||
|
||||
override fun getDefaultMediaType(): MediaType = MEDIATYPE_OPDS_PUBLICATION_JSON
|
||||
|
||||
override fun getBookSeriesLink(bookDto: BookDto): List<WPLinkDto> =
|
||||
listOf(
|
||||
WPLinkDto(
|
||||
href = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray()).path("series/${bookDto.seriesId}").toUriString(),
|
||||
type = MEDIATYPE_OPDS_JSON_VALUE,
|
||||
),
|
||||
)
|
||||
|
||||
fun generateOpdsAuthDocument() =
|
||||
AuthenticationDocumentDto(
|
||||
id = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path(ROUTE_AUTH).toUriString(),
|
||||
title = "Komga",
|
||||
description = "Enter your email and password to authenticate.",
|
||||
links =
|
||||
listOf(
|
||||
WPLinkDto(rel = "help", href = "https://komga.org"),
|
||||
WPLinkDto(rel = "logo", href = ServletUriComponentsBuilder.fromCurrentContextPath().path("android-chrome-512x512.png").toUriString()),
|
||||
),
|
||||
authentication =
|
||||
listOf(
|
||||
AuthenticationFlowDto(
|
||||
type = AuthenticationType.BASIC,
|
||||
labels = LabelsDto(login = "Email", password = "Password"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -14,8 +14,6 @@ import org.gotson.komga.infrastructure.image.ImageConverter
|
|||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_DIVINA_JSON
|
||||
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_DIVINA_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_WEBPUB_JSON
|
||||
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_WEBPUB_JSON_VALUE
|
||||
import org.gotson.komga.interfaces.api.dto.OpdsLinkRel
|
||||
|
|
@ -33,55 +31,35 @@ import org.gotson.komga.interfaces.api.rest.dto.BookDto
|
|||
import org.gotson.komga.language.toZonedDateTime
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import org.gotson.komga.domain.model.MediaType as KomgaMediaType
|
||||
|
||||
@Service
|
||||
@Component
|
||||
class WebPubGenerator(
|
||||
@Qualifier("thumbnailType") private val thumbnailType: ImageType,
|
||||
private val imageConverter: ImageConverter,
|
||||
private val bookAnalyzer: BookAnalyzer,
|
||||
private val mediaRepository: MediaRepository,
|
||||
protected val pathSegments: List<String> = listOf("api", "v1"),
|
||||
) {
|
||||
private val wpKnownRoles =
|
||||
listOf(
|
||||
"author",
|
||||
"translator",
|
||||
"editor",
|
||||
"artist",
|
||||
"illustrator",
|
||||
"letterer",
|
||||
"penciler",
|
||||
"penciller",
|
||||
"colorist",
|
||||
"inker",
|
||||
)
|
||||
|
||||
private val recommendedImageMediaTypes = listOf("image/jpeg", "image/png", "image/gif")
|
||||
|
||||
private fun BookDto.toBasePublicationDto(includeOpdsLinks: Boolean = false): WPPublicationDto {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("api", "v1")
|
||||
protected fun toBasePublicationDto(bookDto: BookDto): WPPublicationDto {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray())
|
||||
return WPPublicationDto(
|
||||
mediaType = MEDIATYPE_OPDS_PUBLICATION_JSON,
|
||||
mediaType = getDefaultMediaType(),
|
||||
context = "https://readium.org/webpub-manifest/context.jsonld",
|
||||
metadata = toWPMetadataDto(includeOpdsLinks).withAuthors(metadata.authors),
|
||||
links = toWPLinkDtos(uriBuilder),
|
||||
metadata = toWPMetadataDto(bookDto).withAuthors(bookDto.metadata.authors),
|
||||
links = bookDto.toWPLinkDtos(uriBuilder),
|
||||
)
|
||||
}
|
||||
|
||||
fun toOpdsPublicationDto(
|
||||
bookDto: BookDto,
|
||||
includeOpdsLinks: Boolean = false,
|
||||
): WPPublicationDto {
|
||||
return bookDto.toBasePublicationDto(includeOpdsLinks).copy(images = buildThumbnailLinkDtos(bookDto.id))
|
||||
}
|
||||
protected open fun getDefaultMediaType(): MediaType = MEDIATYPE_WEBPUB_JSON
|
||||
|
||||
private fun buildThumbnailLinkDtos(bookId: String) =
|
||||
protected fun buildThumbnailLinkDtos(bookId: String) =
|
||||
listOf(
|
||||
WPLinkDto(
|
||||
href = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("api", "v1").path("books/$bookId/thumbnail").toUriString(),
|
||||
href = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray()).path("books/$bookId/thumbnail").toUriString(),
|
||||
type = thumbnailType.mediaType,
|
||||
),
|
||||
)
|
||||
|
|
@ -91,8 +69,8 @@ class WebPubGenerator(
|
|||
media: Media,
|
||||
seriesMetadata: SeriesMetadata,
|
||||
): WPPublicationDto {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("api", "v1")
|
||||
return bookDto.toBasePublicationDto().let {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray())
|
||||
return toBasePublicationDto(bookDto).let {
|
||||
val pages = if (media.profile == MediaProfile.PDF) bookAnalyzer.getPdfPagesDynamic(media) else media.pages
|
||||
it.copy(
|
||||
mediaType = MEDIATYPE_DIVINA_JSON,
|
||||
|
|
@ -128,8 +106,8 @@ class WebPubGenerator(
|
|||
media: Media,
|
||||
seriesMetadata: SeriesMetadata,
|
||||
): WPPublicationDto {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("api", "v1")
|
||||
return bookDto.toBasePublicationDto().let {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray())
|
||||
return toBasePublicationDto(bookDto).let {
|
||||
it.copy(
|
||||
mediaType = MEDIATYPE_WEBPUB_JSON,
|
||||
metadata = it.metadata.withSeriesMetadata(seriesMetadata).copy(conformsTo = PROFILE_PDF),
|
||||
|
|
@ -150,14 +128,14 @@ class WebPubGenerator(
|
|||
media: Media,
|
||||
seriesMetadata: SeriesMetadata,
|
||||
): WPPublicationDto {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("api", "v1")
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment(*pathSegments.toTypedArray())
|
||||
val extension =
|
||||
when {
|
||||
media.extension is ProxyExtension && media.extension.proxyForType<MediaExtensionEpub>() -> mediaRepository.findExtensionByIdOrNull(media.bookId) as? MediaExtensionEpub
|
||||
media.extension is MediaExtensionEpub -> media.extension
|
||||
else -> null
|
||||
}
|
||||
return bookDto.toBasePublicationDto().let { publication ->
|
||||
return toBasePublicationDto(bookDto).let { publication ->
|
||||
publication.copy(
|
||||
mediaType = MEDIATYPE_WEBPUB_JSON,
|
||||
metadata =
|
||||
|
|
@ -204,36 +182,30 @@ class WebPubGenerator(
|
|||
children = children.map { it.toWPLinkDto(uriBuilder) },
|
||||
)
|
||||
|
||||
private fun BookDto.toWPMetadataDto(includeOpdsLinks: Boolean = false) =
|
||||
protected open fun toWPMetadataDto(bookDto: BookDto) =
|
||||
WPMetadataDto(
|
||||
title = metadata.title,
|
||||
description = metadata.summary,
|
||||
numberOfPages = this.media.pagesCount,
|
||||
modified = lastModified.toZonedDateTime(),
|
||||
published = metadata.releaseDate,
|
||||
subject = metadata.tags.toList(),
|
||||
identifier = if (metadata.isbn.isNotBlank()) "urn:isbn:${metadata.isbn}" else null,
|
||||
title = bookDto.metadata.title,
|
||||
description = bookDto.metadata.summary,
|
||||
numberOfPages = bookDto.media.pagesCount,
|
||||
modified = bookDto.lastModified.toZonedDateTime(),
|
||||
published = bookDto.metadata.releaseDate,
|
||||
subject = bookDto.metadata.tags.toList(),
|
||||
identifier = if (bookDto.metadata.isbn.isNotBlank()) "urn:isbn:${bookDto.metadata.isbn}" else null,
|
||||
belongsTo =
|
||||
WPBelongsToDto(
|
||||
series =
|
||||
listOf(
|
||||
WPContributorDto(
|
||||
seriesTitle,
|
||||
metadata.numberSort,
|
||||
if (includeOpdsLinks)
|
||||
listOf(
|
||||
WPLinkDto(
|
||||
href = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path("series/$seriesId").toUriString(),
|
||||
type = MEDIATYPE_OPDS_JSON_VALUE,
|
||||
),
|
||||
)
|
||||
else
|
||||
emptyList(),
|
||||
bookDto.seriesTitle,
|
||||
bookDto.metadata.numberSort,
|
||||
getBookSeriesLink(bookDto),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
protected open fun getBookSeriesLink(bookDto: BookDto): List<WPLinkDto> = emptyList()
|
||||
|
||||
private fun WPMetadataDto.withSeriesMetadata(seriesMetadata: SeriesMetadata) =
|
||||
copy(
|
||||
language = seriesMetadata.language,
|
||||
|
|
@ -284,4 +256,21 @@ class WebPubGenerator(
|
|||
MediaProfile.EPUB -> MEDIATYPE_WEBPUB_JSON_VALUE
|
||||
null -> MEDIATYPE_WEBPUB_JSON_VALUE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val wpKnownRoles =
|
||||
listOf(
|
||||
"author",
|
||||
"translator",
|
||||
"editor",
|
||||
"artist",
|
||||
"illustrator",
|
||||
"letterer",
|
||||
"penciler",
|
||||
"penciller",
|
||||
"colorist",
|
||||
"inker",
|
||||
)
|
||||
private val recommendedImageMediaTypes = listOf("image/jpeg", "image/png", "image/gif")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package org.gotson.komga.interfaces.api.dto
|
|||
import org.springframework.http.MediaType
|
||||
|
||||
const val MEDIATYPE_OPDS_JSON_VALUE = "application/opds+json"
|
||||
const val MEDIATYPE_OPDS_PUBLICATION_JSON_VALUE = "application/opds-publication+json"
|
||||
const val MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE = "application/opds-authentication+json"
|
||||
const val MEDIATYPE_DIVINA_JSON_VALUE = "application/divina+json"
|
||||
const val MEDIATYPE_WEBPUB_JSON_VALUE = "application/webpub+json"
|
||||
const val MEDIATYPE_POSITION_LIST_JSON_VALUE = "application/vnd.readium.position-list+json"
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@ class OpdsLinkRel {
|
|||
const val SORT_NEW = "http://opds-spec.org/sort/new"
|
||||
const val SORT_POPULAR = "http://opds-spec.org/sort/popular"
|
||||
const val ACQUISITION = "http://opds-spec.org/acquisition"
|
||||
const val AUTH = "http://opds-spec.org/auth/document"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import org.gotson.komga.domain.persistence.ReferentialRepository
|
|||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.infrastructure.swagger.PageAsQueryParam
|
||||
import org.gotson.komga.interfaces.api.WebPubGenerator
|
||||
import org.gotson.komga.interfaces.api.OpdsGenerator
|
||||
import org.gotson.komga.interfaces.api.checkContentRestriction
|
||||
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.OpdsLinkRel
|
||||
import org.gotson.komga.interfaces.api.dto.WPLinkDto
|
||||
|
|
@ -46,6 +47,7 @@ import java.time.ZoneId
|
|||
import java.time.ZonedDateTime
|
||||
|
||||
private const val ROUTE_CATALOG = "catalog"
|
||||
const val ROUTE_AUTH = "auth"
|
||||
|
||||
private const val RECOMMENDED_ITEMS_NUMBER = 5
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ class Opds2Controller(
|
|||
private val seriesDtoRepository: SeriesDtoRepository,
|
||||
private val bookDtoRepository: BookDtoRepository,
|
||||
private val referentialRepository: ReferentialRepository,
|
||||
private val webPubGenerator: WebPubGenerator,
|
||||
private val opdsGenerator: OpdsGenerator,
|
||||
) {
|
||||
private fun linkStart() =
|
||||
WPLinkDto(
|
||||
|
|
@ -77,8 +79,7 @@ class Opds2Controller(
|
|||
templated = true,
|
||||
)
|
||||
|
||||
private fun uriBuilder(path: String) =
|
||||
ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path(path)
|
||||
private fun uriBuilder(path: String) = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("opds", "v2").path(path)
|
||||
|
||||
private fun linkPage(
|
||||
uriBuilder: UriComponentsBuilder,
|
||||
|
|
@ -105,18 +106,16 @@ class Opds2Controller(
|
|||
private fun linkSelf(
|
||||
path: String,
|
||||
type: String? = null,
|
||||
) =
|
||||
linkSelf(uriBuilder(path), type)
|
||||
) = linkSelf(uriBuilder(path), type)
|
||||
|
||||
private fun linkSelf(
|
||||
uriBuilder: UriComponentsBuilder,
|
||||
type: String? = null,
|
||||
) =
|
||||
WPLinkDto(
|
||||
rel = OpdsLinkRel.SELF,
|
||||
href = uriBuilder.toUriString(),
|
||||
type = type,
|
||||
)
|
||||
) = WPLinkDto(
|
||||
rel = OpdsLinkRel.SELF,
|
||||
href = uriBuilder.toUriString(),
|
||||
type = type,
|
||||
)
|
||||
|
||||
private fun getLibrariesFeedGroup(
|
||||
principal: KomgaPrincipal,
|
||||
|
|
@ -178,7 +177,7 @@ class Opds2Controller(
|
|||
principal.user.id,
|
||||
PageRequest.of(0, RECOMMENDED_ITEMS_NUMBER, Sort.by(Sort.Order.desc("readProgress.readDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val onDeck =
|
||||
bookDtoRepository.findAllOnDeck(
|
||||
|
|
@ -186,7 +185,7 @@ class Opds2Controller(
|
|||
authorizedLibraryIds,
|
||||
Pageable.ofSize(RECOMMENDED_ITEMS_NUMBER),
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val latestBooks =
|
||||
bookDtoRepository.findAll(
|
||||
|
|
@ -198,7 +197,7 @@ class Opds2Controller(
|
|||
principal.user.id,
|
||||
PageRequest.of(0, RECOMMENDED_ITEMS_NUMBER, Sort.by(Sort.Order.desc("createdDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val latestSeries =
|
||||
seriesDtoRepository.findAll(
|
||||
|
|
@ -286,7 +285,7 @@ class Opds2Controller(
|
|||
principal.user.id,
|
||||
PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("readProgress.readDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/keep-reading")
|
||||
|
||||
|
|
@ -323,7 +322,7 @@ class Opds2Controller(
|
|||
authorizedLibraryIds,
|
||||
page,
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/on-deck")
|
||||
|
||||
|
|
@ -364,7 +363,7 @@ class Opds2Controller(
|
|||
principal.user.id,
|
||||
PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("createdDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/books/latest")
|
||||
|
||||
|
|
@ -446,22 +445,18 @@ class Opds2Controller(
|
|||
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.titleSort")))
|
||||
|
||||
val entries =
|
||||
seriesDtoRepository
|
||||
.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
.map { it.toWPLinkDto() }
|
||||
val entries = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions).map { it.toWPLinkDto() }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/browse")
|
||||
|
||||
val publisherLinks =
|
||||
referentialRepository.findAllPublishers(authorizedLibraryIds)
|
||||
.map {
|
||||
WPLinkDto(
|
||||
title = it,
|
||||
href = uriBuilder.cloneBuilder().queryParam("publisher", it).toUriString(),
|
||||
type = MEDIATYPE_OPDS_JSON_VALUE,
|
||||
)
|
||||
}
|
||||
referentialRepository.findAllPublishers(authorizedLibraryIds).map {
|
||||
WPLinkDto(
|
||||
title = it,
|
||||
href = uriBuilder.cloneBuilder().queryParam("publisher", it).toUriString(),
|
||||
type = MEDIATYPE_OPDS_JSON_VALUE,
|
||||
)
|
||||
}
|
||||
|
||||
return FeedDto(
|
||||
metadata =
|
||||
|
|
@ -548,9 +543,7 @@ class Opds2Controller(
|
|||
deleted = false,
|
||||
)
|
||||
|
||||
val entries =
|
||||
seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
.map { it.toWPLinkDto() }
|
||||
val entries = seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageable, principal.user.restrictions).map { it.toWPLinkDto() }
|
||||
|
||||
val uriBuilder = uriBuilder("collections/$id")
|
||||
|
||||
|
|
@ -643,7 +636,7 @@ class Opds2Controller(
|
|||
principal.user.restrictions,
|
||||
)
|
||||
|
||||
val entries = booksPage.map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
val entries = booksPage.map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("readlists/$id")
|
||||
|
||||
|
|
@ -701,22 +694,19 @@ class Opds2Controller(
|
|||
)
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.numberSort")))
|
||||
|
||||
val entries =
|
||||
bookDtoRepository.findAll(bookSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
.map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
val entries = bookDtoRepository.findAll(bookSearch, principal.user.id, pageable, principal.user.restrictions).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("series/$id")
|
||||
|
||||
val tagLinks =
|
||||
referentialRepository.findAllBookTagsBySeries(series.id, null)
|
||||
.map {
|
||||
WPLinkDto(
|
||||
title = it,
|
||||
href = uriBuilder.cloneBuilder().queryParam("tag", it).toUriString(),
|
||||
type = MEDIATYPE_OPDS_JSON_VALUE,
|
||||
rel = if (it == tag) OpdsLinkRel.SELF else null,
|
||||
)
|
||||
}
|
||||
referentialRepository.findAllBookTagsBySeries(series.id, null).map {
|
||||
WPLinkDto(
|
||||
title = it,
|
||||
href = uriBuilder.cloneBuilder().queryParam("tag", it).toUriString(),
|
||||
type = MEDIATYPE_OPDS_JSON_VALUE,
|
||||
rel = if (it == tag) OpdsLinkRel.SELF else null,
|
||||
)
|
||||
}
|
||||
|
||||
FeedDto(
|
||||
metadata =
|
||||
|
|
@ -749,32 +739,29 @@ class Opds2Controller(
|
|||
val pageable = PageRequest.of(0, 20, Sort.by("relevance"))
|
||||
|
||||
val resultsSeries =
|
||||
seriesDtoRepository
|
||||
.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = query,
|
||||
oneshot = false,
|
||||
deleted = false,
|
||||
),
|
||||
principal.user.id,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
)
|
||||
.map { it.toWPLinkDto() }
|
||||
seriesDtoRepository.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = query,
|
||||
oneshot = false,
|
||||
deleted = false,
|
||||
),
|
||||
principal.user.id,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
).map { it.toWPLinkDto() }
|
||||
|
||||
val resultsBooks =
|
||||
bookDtoRepository
|
||||
.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = query,
|
||||
deleted = false,
|
||||
),
|
||||
principal.user.id,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
).map { webPubGenerator.toOpdsPublicationDto(it, true) }
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = query,
|
||||
deleted = false,
|
||||
),
|
||||
principal.user.id,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val resultsCollections =
|
||||
collectionRepository.findAll(
|
||||
|
|
@ -815,6 +802,9 @@ class Opds2Controller(
|
|||
)
|
||||
}
|
||||
|
||||
@GetMapping(value = [ROUTE_AUTH], produces = [MEDIATYPE_OPDS_AUTHENTICATION_JSON_VALUE])
|
||||
fun getAuthDocument() = opdsGenerator.generateOpdsAuthDocument()
|
||||
|
||||
private fun Library.toWPLinkDto(): WPLinkDto =
|
||||
WPLinkDto(
|
||||
title = name,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package org.gotson.komga.interfaces.api.opds.v2.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
import org.gotson.komga.interfaces.api.dto.WPLinkDto
|
||||
|
||||
/**
|
||||
* https://drafts.opds.io/authentication-for-opds-1.0.html#23-syntax
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
data class AuthenticationDocumentDto(
|
||||
/**
|
||||
* A list of supported Authentication Flows as defined in section 3. Authentication Flows.
|
||||
*/
|
||||
val authentication: List<AuthenticationFlowDto>,
|
||||
/**
|
||||
* Title of the Catalog being accessed.
|
||||
*/
|
||||
val title: String,
|
||||
/**
|
||||
* Unique identifier for the Catalog provider and canonical location for the Authentication Document.
|
||||
*/
|
||||
val id: String,
|
||||
/**
|
||||
* A description of the service being displayed to the user.
|
||||
*/
|
||||
val description: String? = null,
|
||||
/**
|
||||
* An Authentication Document may also contain a links object.
|
||||
* This is used to associate the Authentication Document with resources that are not locally available.
|
||||
*/
|
||||
val links: List<WPLinkDto> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* In addition to the Authentication Document, this specification also defines multiple scenarios to handle how the client is authenticated.
|
||||
* Each Authentication Document contains at least one Authentication Object that describes how a client can leverage an Authentication Flow.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
data class AuthenticationFlowDto(
|
||||
val type: AuthenticationType,
|
||||
val labels: LabelsDto? = null,
|
||||
val links: List<WPLinkDto> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class LabelsDto(
|
||||
val login: String? = null,
|
||||
val password: String? = null,
|
||||
)
|
||||
|
||||
enum class AuthenticationType(
|
||||
@get:JsonValue val value: String,
|
||||
) {
|
||||
BASIC("http://opds-spec.org/auth/basic"),
|
||||
OAUTH2_IMPLICIT("http://opds-spec.org/auth/oauth/implicit"),
|
||||
OAUTH2_PASSWORD("http://opds-spec.org/auth/oauth/password"),
|
||||
}
|
||||
|
|
@ -53,9 +53,11 @@ import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
|
|||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
|
||||
import org.gotson.komga.infrastructure.web.setCachePrivate
|
||||
import org.gotson.komga.interfaces.api.OpdsGenerator
|
||||
import org.gotson.komga.interfaces.api.WebPubGenerator
|
||||
import org.gotson.komga.interfaces.api.checkContentRestriction
|
||||
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_DIVINA_JSON_VALUE
|
||||
import org.gotson.komga.interfaces.api.dto.MEDIATYPE_OPDS_PUBLICATION_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
|
||||
|
|
@ -135,6 +137,7 @@ class BookController(
|
|||
private val eventPublisher: ApplicationEventPublisher,
|
||||
private val thumbnailBookRepository: ThumbnailBookRepository,
|
||||
private val webPubGenerator: WebPubGenerator,
|
||||
private val opdsGenerator: OpdsGenerator,
|
||||
) {
|
||||
@PageableAsQueryParam
|
||||
@GetMapping("api/v1/books")
|
||||
|
|
@ -403,6 +406,8 @@ class BookController(
|
|||
"api/v1/books/{bookId}/file",
|
||||
"api/v1/books/{bookId}/file/*",
|
||||
"opds/v1.2/books/{bookId}/file/*",
|
||||
"opds/v2/books/{bookId}/file",
|
||||
"opds/v2/books/{bookId}/file/*",
|
||||
],
|
||||
produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE],
|
||||
)
|
||||
|
|
@ -495,7 +500,10 @@ class BookController(
|
|||
getBookPageInternal(bookId, pageNumber + 1, convertTo, request, principal, null)
|
||||
|
||||
@ApiResponse(content = [Content(mediaType = "image/*", schema = Schema(type = "string", format = "binary"))])
|
||||
@GetMapping("api/v1/books/{bookId}/pages/{pageNumber}", produces = [MediaType.ALL_VALUE])
|
||||
@GetMapping(
|
||||
value = ["api/v1/books/{bookId}/pages/{pageNumber}"],
|
||||
produces = [MediaType.ALL_VALUE],
|
||||
)
|
||||
@PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')")
|
||||
fun getBookPage(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -517,6 +525,26 @@ class BookController(
|
|||
): ResponseEntity<ByteArray> =
|
||||
getBookPageInternal(bookId, if (zeroBasedIndex) pageNumber + 1 else pageNumber, convertTo, request, principal, acceptHeaders)
|
||||
|
||||
@ApiResponse(content = [Content(mediaType = "image/*", schema = Schema(type = "string", format = "binary"))])
|
||||
@GetMapping(
|
||||
value = ["opds/v2/books/{bookId}/pages/{pageNumber}"],
|
||||
produces = [MediaType.ALL_VALUE],
|
||||
)
|
||||
@PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')")
|
||||
fun getBookPageOpdsv2(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
request: ServletWebRequest,
|
||||
@PathVariable bookId: String,
|
||||
@PathVariable pageNumber: Int,
|
||||
@Parameter(
|
||||
description = "Convert the image to the provided format.",
|
||||
schema = Schema(allowableValues = ["jpeg", "png"]),
|
||||
)
|
||||
@RequestParam(value = "convert", required = false)
|
||||
convertTo: String?,
|
||||
): ResponseEntity<ByteArray> =
|
||||
getBookPageInternal(bookId, pageNumber, convertTo, request, principal, null)
|
||||
|
||||
private fun getBookPageInternal(
|
||||
bookId: String,
|
||||
pageNumber: Int,
|
||||
|
|
@ -582,7 +610,10 @@ class BookController(
|
|||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
value = ["api/v1/books/{bookId}/pages/{pageNumber}/raw"],
|
||||
value = [
|
||||
"api/v1/books/{bookId}/pages/{pageNumber}/raw",
|
||||
"opds/v2/books/{bookId}/pages/{pageNumber}/raw",
|
||||
],
|
||||
produces = [MediaType.ALL_VALUE],
|
||||
)
|
||||
@PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')")
|
||||
|
|
@ -687,18 +718,42 @@ class BookController(
|
|||
fun getWebPubManifest(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<WPPublicationDto> =
|
||||
): ResponseEntity<WPPublicationDto> {
|
||||
val manifest = getWebPubManifestInternal(principal, bookId, webPubGenerator)
|
||||
return ResponseEntity.ok()
|
||||
.contentType(manifest.mediaType)
|
||||
.body(manifest)
|
||||
}
|
||||
|
||||
@GetMapping(
|
||||
value = ["opds/v2/books/{bookId}/manifest"],
|
||||
produces = [MEDIATYPE_OPDS_PUBLICATION_JSON_VALUE],
|
||||
)
|
||||
fun getWebPubManifestOpds(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestInternal(principal, bookId, opdsGenerator)
|
||||
|
||||
private fun getWebPubManifestInternal(
|
||||
principal: KomgaPrincipal,
|
||||
bookId: String,
|
||||
webPubGenerator: WebPubGenerator,
|
||||
) =
|
||||
mediaRepository.findByIdOrNull(bookId)?.let { media ->
|
||||
when (KomgaMediaType.fromMediaType(media.mediaType)?.profile) {
|
||||
MediaProfile.DIVINA -> getWebPubManifestDivina(principal, bookId)
|
||||
MediaProfile.PDF -> getWebPubManifestPdf(principal, bookId)
|
||||
MediaProfile.EPUB -> getWebPubManifestEpub(principal, bookId)
|
||||
MediaProfile.DIVINA -> getWebPubManifestDivinaInternal(principal, bookId, webPubGenerator)
|
||||
MediaProfile.PDF -> getWebPubManifestPdfInternal(principal, bookId, webPubGenerator)
|
||||
MediaProfile.EPUB -> getWebPubManifestEpubInternal(principal, bookId, webPubGenerator)
|
||||
null -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
value = ["api/v1/books/{bookId}/resource/{*resource}"],
|
||||
value = [
|
||||
"api/v1/books/{bookId}/resource/{*resource}",
|
||||
"opds/v2/books/{bookId}/resource/{*resource}",
|
||||
],
|
||||
produces = ["*/*"],
|
||||
)
|
||||
fun getBookResource(
|
||||
|
|
@ -821,19 +876,32 @@ class BookController(
|
|||
fun getWebPubManifestEpub(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<WPPublicationDto> =
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestEpubInternal(principal, bookId, webPubGenerator)
|
||||
|
||||
@GetMapping(
|
||||
value = ["opds/v2/books/{bookId}/manifest/epub"],
|
||||
produces = [MEDIATYPE_OPDS_PUBLICATION_JSON_VALUE],
|
||||
)
|
||||
fun getWebPubManifestEpubOpds(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestEpubInternal(principal, bookId, opdsGenerator)
|
||||
|
||||
private fun getWebPubManifestEpubInternal(
|
||||
principal: KomgaPrincipal,
|
||||
bookId: String,
|
||||
webPubGenerator: WebPubGenerator,
|
||||
) =
|
||||
bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let { bookDto ->
|
||||
if (bookDto.media.mediaProfile != MediaProfile.EPUB.name) throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Book media type '${bookDto.media.mediaType}' not compatible with requested profile")
|
||||
principal.user.checkContentRestriction(bookDto)
|
||||
val manifest =
|
||||
webPubGenerator.toManifestEpub(
|
||||
bookDto,
|
||||
mediaRepository.findById(bookId),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
ResponseEntity.ok()
|
||||
.contentType(manifest.mediaType)
|
||||
.body(manifest)
|
||||
webPubGenerator.toManifestEpub(
|
||||
bookDto,
|
||||
mediaRepository.findById(bookId),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
|
|
@ -843,19 +911,32 @@ class BookController(
|
|||
fun getWebPubManifestPdf(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<WPPublicationDto> =
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestPdfInternal(principal, bookId, webPubGenerator)
|
||||
|
||||
@GetMapping(
|
||||
value = ["opds/v2/books/{bookId}/manifest/pdf"],
|
||||
produces = [MEDIATYPE_OPDS_PUBLICATION_JSON_VALUE],
|
||||
)
|
||||
fun getWebPubManifestPdfOpds(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestPdfInternal(principal, bookId, opdsGenerator)
|
||||
|
||||
private fun getWebPubManifestPdfInternal(
|
||||
principal: KomgaPrincipal,
|
||||
bookId: String,
|
||||
webPubGenerator: WebPubGenerator,
|
||||
) =
|
||||
bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let { bookDto ->
|
||||
if (bookDto.media.mediaProfile != MediaProfile.PDF.name) throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Book media type '${bookDto.media.mediaType}' not compatible with requested profile")
|
||||
principal.user.checkContentRestriction(bookDto)
|
||||
val manifest =
|
||||
webPubGenerator.toManifestPdf(
|
||||
bookDto,
|
||||
mediaRepository.findById(bookDto.id),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
ResponseEntity.ok()
|
||||
.contentType(manifest.mediaType)
|
||||
.body(manifest)
|
||||
webPubGenerator.toManifestPdf(
|
||||
bookDto,
|
||||
mediaRepository.findById(bookDto.id),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
|
|
@ -865,18 +946,31 @@ class BookController(
|
|||
fun getWebPubManifestDivina(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<WPPublicationDto> =
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestDivinaInternal(principal, bookId, webPubGenerator)
|
||||
|
||||
@GetMapping(
|
||||
value = ["opds/v2/books/{bookId}/manifest/divina"],
|
||||
produces = [MEDIATYPE_OPDS_PUBLICATION_JSON_VALUE],
|
||||
)
|
||||
fun getWebPubManifestDivinaOpds(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): WPPublicationDto =
|
||||
getWebPubManifestDivinaInternal(principal, bookId, opdsGenerator)
|
||||
|
||||
private fun getWebPubManifestDivinaInternal(
|
||||
principal: KomgaPrincipal,
|
||||
bookId: String,
|
||||
webPubGenerator: WebPubGenerator,
|
||||
) =
|
||||
bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let { bookDto ->
|
||||
principal.user.checkContentRestriction(bookDto)
|
||||
val manifest =
|
||||
webPubGenerator.toManifestDivina(
|
||||
bookDto,
|
||||
mediaRepository.findById(bookDto.id),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
ResponseEntity.ok()
|
||||
.contentType(manifest.mediaType)
|
||||
.body(manifest)
|
||||
webPubGenerator.toManifestDivina(
|
||||
bookDto,
|
||||
mediaRepository.findById(bookDto.id),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@PostMapping("api/v1/books/{bookId}/analyze")
|
||||
|
|
|
|||
Loading…
Reference in a new issue