mirror of
https://github.com/gotson/komga.git
synced 2025-12-30 20:33:06 +01:00
feat(opds2): generate PDF profile webpub manifest
This commit is contained in:
parent
2c33b3e0f1
commit
7205b1372d
6 changed files with 96 additions and 7 deletions
|
|
@ -9,6 +9,6 @@ enum class MediaType(val type: String, val fileExtension: String, val exportType
|
|||
;
|
||||
|
||||
companion object {
|
||||
fun fromMediaType(mediaType: String): MediaType? = values().firstOrNull { it.type == mediaType }
|
||||
fun fromMediaType(mediaType: String?): MediaType? = values().firstOrNull { it.type == mediaType }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import org.gotson.komga.domain.model.Media
|
|||
|
||||
interface MediaRepository {
|
||||
fun findById(bookId: String): Media
|
||||
fun findByIdOrNull(bookId: String): Media?
|
||||
|
||||
fun findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(libraryId: String, mediaTypes: Collection<String>, pageHashing: Int): Collection<Pair<String, String>>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ class MediaDao(
|
|||
private val groupFields = arrayOf(*m.fields(), *p.fields())
|
||||
|
||||
override fun findById(bookId: String): Media =
|
||||
find(dsl, bookId)!!
|
||||
|
||||
override fun findByIdOrNull(bookId: String): Media? =
|
||||
find(dsl, bookId)
|
||||
|
||||
override fun findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(libraryId: String, mediaTypes: Collection<String>, pageHashing: Int): Collection<Pair<String, String>> {
|
||||
|
|
@ -64,7 +67,7 @@ class MediaDao(
|
|||
.fetch()
|
||||
.map { Pair(it[m.BOOK_ID], it[m.PAGE_COUNT]) }
|
||||
|
||||
private fun find(dsl: DSLContext, bookId: String): Media =
|
||||
private fun find(dsl: DSLContext, bookId: String): Media? =
|
||||
dsl.select(*groupFields)
|
||||
.from(m)
|
||||
.leftJoin(p).on(m.BOOK_ID.eq(p.BOOK_ID))
|
||||
|
|
@ -81,7 +84,7 @@ class MediaDao(
|
|||
.map { it.fileName }
|
||||
|
||||
mr.toDomain(pr.filterNot { it.bookId == null }.map { it.toDomain() }, files)
|
||||
}.first()
|
||||
}.firstOrNull()
|
||||
|
||||
@Transactional
|
||||
override fun insert(media: Media) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import org.springframework.http.MediaType
|
|||
|
||||
const val MEDIATYPE_OPDS_JSON_VALUE = "application/opds+json"
|
||||
const val MEDIATYPE_DIVINA_JSON_VALUE = "application/divina+json"
|
||||
const val MEDIATYPE_WEBPUB_JSON_VALUE = "application/webpub+json"
|
||||
|
||||
const val PROFILE_DIVINA = "https://readium.org/webpub-manifest/profiles/divina"
|
||||
const val PROFILE_PDF = "https://readium.org/webpub-manifest/profiles/pdf"
|
||||
|
||||
val MEDIATYPE_OPDS_PUBLICATION_JSON = MediaType("application", "opds-publication+json")
|
||||
val MEDIATYPE_DIVINA_JSON = MediaType("application", "webpub+json")
|
||||
val MEDIATYPE_DIVINA_JSON = MediaType("application", "divina+json")
|
||||
val MEDIATYPE_WEBPUB_JSON = MediaType("application", "webpub+json")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.interfaces.api.dto
|
|||
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaType.PDF
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT
|
||||
import org.gotson.komga.domain.model.SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT
|
||||
|
|
@ -15,6 +16,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder
|
|||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import org.gotson.komga.domain.model.MediaType as KMediaType
|
||||
import org.gotson.komga.domain.model.MediaType.Companion as KomgaMediaType
|
||||
|
||||
val wpKnownRoles = listOf(
|
||||
|
|
@ -76,6 +78,24 @@ fun BookDto.toManifestDivina(canConvertMediaType: (String, String) -> Boolean, m
|
|||
}
|
||||
}
|
||||
|
||||
fun BookDto.toManifestPdf(media: Media, seriesMetadata: SeriesMetadata): WPPublicationDto {
|
||||
val uriBuilder = ServletUriComponentsBuilder.fromCurrentContextPath().pathSegment("api", "v1")
|
||||
return toOpdsPublicationDto().let {
|
||||
it.copy(
|
||||
mediaType = MEDIATYPE_WEBPUB_JSON,
|
||||
metadata = it.metadata
|
||||
.withSeriesMetadata(seriesMetadata)
|
||||
.copy(conformsTo = PROFILE_PDF),
|
||||
readingOrder = List(media.pages.size) { index: Int ->
|
||||
WPLinkDto(
|
||||
href = uriBuilder.cloneBuilder().path("books/$id/pages/${index + 1}/raw").toUriString(),
|
||||
type = PDF.type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BookDto.toWPMetadataDto(includeOpdsLinks: Boolean = false) = WPMetadataDto(
|
||||
title = metadata.title,
|
||||
description = metadata.summary,
|
||||
|
|
@ -130,10 +150,26 @@ private fun WPMetadataDto.withAuthors(authors: List<AuthorDto>): WPMetadataDto {
|
|||
}
|
||||
|
||||
private fun BookDto.toWPLinkDtos(uriBuilder: UriComponentsBuilder): List<WPLinkDto> {
|
||||
val komgaMediaType = KomgaMediaType.fromMediaType(media.mediaType)
|
||||
val download = WPLinkDto(rel = OpdsLinkRel.ACQUISITION, type = media.mediaType, href = uriBuilder.cloneBuilder().path("books/$id/file").toUriString())
|
||||
|
||||
return listOfNotNull(
|
||||
WPLinkDto(rel = OpdsLinkRel.SELF, href = uriBuilder.cloneBuilder().path("books/$id/manifest/divina").toUriString(), type = MEDIATYPE_DIVINA_JSON_VALUE),
|
||||
// most appropriate manifest
|
||||
WPLinkDto(rel = OpdsLinkRel.SELF, href = uriBuilder.cloneBuilder().path("books/$id/manifest").toUriString(), type = mediaTypeToWebPub(komgaMediaType)),
|
||||
// PDF is also available under the Divina profile
|
||||
if (komgaMediaType == PDF) WPLinkDto(href = uriBuilder.cloneBuilder().path("books/$id/manifest/divina").toUriString(), type = MEDIATYPE_DIVINA_JSON_VALUE) else null,
|
||||
// main acquisition link
|
||||
download,
|
||||
KomgaMediaType.fromMediaType(media.mediaType)?.let { download.copy(type = it.exportType) },
|
||||
// extra acquisition link with a different export type, useful for CBR/CBZ
|
||||
komgaMediaType?.let { download.copy(type = it.exportType) },
|
||||
).distinct()
|
||||
}
|
||||
|
||||
private fun mediaTypeToWebPub(mediaType: KMediaType?): String = when (mediaType) {
|
||||
KMediaType.ZIP -> MEDIATYPE_DIVINA_JSON_VALUE
|
||||
KMediaType.RAR_GENERIC -> MEDIATYPE_DIVINA_JSON_VALUE
|
||||
KMediaType.RAR_4 -> MEDIATYPE_DIVINA_JSON_VALUE
|
||||
KMediaType.EPUB -> MEDIATYPE_DIVINA_JSON_VALUE
|
||||
PDF -> MEDIATYPE_WEBPUB_JSON_VALUE
|
||||
null -> MEDIATYPE_WEBPUB_JSON_VALUE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ import org.gotson.komga.domain.model.KomgaUser
|
|||
import org.gotson.komga.domain.model.MarkSelectedPreference
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.MediaType.EPUB
|
||||
import org.gotson.komga.domain.model.MediaType.PDF
|
||||
import org.gotson.komga.domain.model.MediaType.RAR_4
|
||||
import org.gotson.komga.domain.model.MediaType.RAR_GENERIC
|
||||
import org.gotson.komga.domain.model.MediaType.ZIP
|
||||
import org.gotson.komga.domain.model.MediaUnsupportedException
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
|
|
@ -46,8 +51,10 @@ import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
|
|||
import org.gotson.komga.infrastructure.web.setCachePrivate
|
||||
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_WEBPUB_JSON_VALUE
|
||||
import org.gotson.komga.interfaces.api.dto.WPPublicationDto
|
||||
import org.gotson.komga.interfaces.api.dto.toManifestDivina
|
||||
import org.gotson.komga.interfaces.api.dto.toManifestPdf
|
||||
import org.gotson.komga.interfaces.api.persistence.BookDtoRepository
|
||||
import org.gotson.komga.interfaces.api.rest.dto.BookDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.BookImportBatchDto
|
||||
|
|
@ -95,6 +102,7 @@ import java.nio.file.NoSuchFileException
|
|||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.io.path.name
|
||||
import org.gotson.komga.domain.model.MediaType as KomgaMediaType
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -611,10 +619,48 @@ class BookController(
|
|||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
value = ["api/v1/books/{bookId}/manifest"],
|
||||
produces = [MEDIATYPE_WEBPUB_JSON_VALUE, MEDIATYPE_DIVINA_JSON_VALUE],
|
||||
)
|
||||
fun getWebPubManifest(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<WPPublicationDto> =
|
||||
mediaRepository.findByIdOrNull(bookId)?.let { media ->
|
||||
when (KomgaMediaType.fromMediaType(media.mediaType)) {
|
||||
ZIP -> getWebPubManifestDivina(principal, bookId)
|
||||
RAR_GENERIC -> getWebPubManifestDivina(principal, bookId)
|
||||
RAR_4 -> getWebPubManifestDivina(principal, bookId)
|
||||
EPUB -> getWebPubManifestDivina(principal, bookId)
|
||||
PDF -> getWebPubManifestPdf(principal, bookId)
|
||||
null -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
value = ["api/v1/books/{bookId}/manifest/pdf"],
|
||||
produces = [MEDIATYPE_WEBPUB_JSON_VALUE],
|
||||
)
|
||||
fun getWebPubManifestPdf(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable bookId: String,
|
||||
): ResponseEntity<WPPublicationDto> =
|
||||
bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let { bookDto ->
|
||||
if (bookDto.media.mediaType != KomgaMediaType.PDF.type) throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Book media type '${bookDto.media.mediaType}' not compatible with requested profile")
|
||||
principal.user.checkContentRestriction(bookDto)
|
||||
val manifest = bookDto.toManifestPdf(
|
||||
mediaRepository.findById(bookDto.id),
|
||||
seriesMetadataRepository.findById(bookDto.seriesId),
|
||||
)
|
||||
ResponseEntity.ok()
|
||||
.contentType(manifest.mediaType)
|
||||
.body(manifest)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
value = [
|
||||
"api/v1/books/{bookId}/manifest/divina",
|
||||
"api/v1/books/{bookId}/manifest",
|
||||
],
|
||||
produces = [MEDIATYPE_DIVINA_JSON_VALUE],
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue