feat(api): get duplicate books by filehash

This commit is contained in:
Gauthier Roebroeck 2021-12-31 14:34:33 +08:00
parent ba5072a731
commit 3c97c20481
4 changed files with 105 additions and 48 deletions

View file

@ -64,6 +64,7 @@ class BookDtoDao(
"lastModifiedDate" to b.LAST_MODIFIED_DATE, "lastModifiedDate" to b.LAST_MODIFIED_DATE,
"fileSize" to b.FILE_SIZE, "fileSize" to b.FILE_SIZE,
"size" to b.FILE_SIZE, "size" to b.FILE_SIZE,
"fileHash" to b.FILE_HASH,
"url" to b.URL.noCase(), "url" to b.URL.noCase(),
"media.status" to m.STATUS.noCase(), "media.status" to m.STATUS.noCase(),
"media.comment" to m.COMMENT.noCase(), "media.comment" to m.COMMENT.noCase(),
@ -87,7 +88,7 @@ class BookDtoDao(
userId: String, userId: String,
filterOnLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?,
search: BookSearchWithReadProgress, search: BookSearchWithReadProgress,
pageable: Pageable pageable: Pageable,
): Page<BookDto> { ): Page<BookDto> {
val conditions = rlb.READLIST_ID.eq(readListId).and(search.toCondition()) val conditions = rlb.READLIST_ID.eq(readListId).and(search.toCondition())
@ -139,7 +140,7 @@ class BookDtoDao(
dtos, dtos,
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort) if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort)
else PageRequest.of(0, maxOf(count, 20), pageSort), else PageRequest.of(0, maxOf(count, 20), pageSort),
count.toLong() count.toLong(),
) )
} }
@ -159,7 +160,7 @@ class BookDtoDao(
readListId: String, readListId: String,
bookId: String, bookId: String,
userId: String, userId: String,
filterOnLibraryIds: Collection<String>? filterOnLibraryIds: Collection<String>?,
): BookDto? = ): BookDto? =
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = false) findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = false)
@ -167,7 +168,7 @@ class BookDtoDao(
readListId: String, readListId: String,
bookId: String, bookId: String,
userId: String, userId: String,
filterOnLibraryIds: Collection<String>? filterOnLibraryIds: Collection<String>?,
): BookDto? = ): BookDto? =
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true) findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true)
@ -200,7 +201,34 @@ class BookDtoDao(
return PageImpl( return PageImpl(
dtos, dtos,
PageRequest.of(pageable.pageNumber, pageable.pageSize, pageable.sort), PageRequest.of(pageable.pageNumber, pageable.pageSize, pageable.sort),
seriesIds.size.toLong() seriesIds.size.toLong(),
)
}
override fun findAllDuplicates(userId: String, pageable: Pageable): Page<BookDto> {
val hashes = dsl.select(b.FILE_HASH, DSL.count(b.FILE_HASH))
.from(b)
.where(b.FILE_HASH.ne(""))
.groupBy(b.FILE_HASH)
.having(DSL.count(b.FILE_HASH).gt(1))
.fetch()
.associate { it.value1() to it.value2() }
val count = hashes.values.sum()
val orderBy = pageable.sort.toOrderBy(sorts)
val dtos = selectBase(userId)
.where(b.FILE_HASH.`in`(hashes.keys))
.orderBy(orderBy)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchAndMap()
val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted()
return PageImpl(
dtos,
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort)
else PageRequest.of(0, maxOf(count, 20), pageSort),
count.toLong(),
) )
} }
@ -229,7 +257,7 @@ class BookDtoDao(
bookId: String, bookId: String,
userId: String, userId: String,
filterOnLibraryIds: Collection<String>?, filterOnLibraryIds: Collection<String>?,
next: Boolean next: Boolean,
): BookDto? { ): BookDto? {
val numberSort = dsl.select(rlb.NUMBER) val numberSort = dsl.select(rlb.NUMBER)
.from(b) .from(b)
@ -254,7 +282,7 @@ class BookDtoDao(
*b.fields(), *b.fields(),
*m.fields(), *m.fields(),
*d.fields(), *d.fields(),
*r.fields() *r.fields(),
).apply { if (joinConditions.selectReadListNumber) select(rlb.NUMBER) } ).apply { if (joinConditions.selectReadListNumber) select(rlb.NUMBER) }
.from(b) .from(b)
.leftJoin(m).on(b.ID.eq(m.BOOK_ID)) .leftJoin(m).on(b.ID.eq(m.BOOK_ID))
@ -354,6 +382,7 @@ class BookDtoDao(
metadata = metadata, metadata = metadata,
readProgress = readProgress, readProgress = readProgress,
deleted = deletedDate != null, deleted = deletedDate != null,
fileHash = fileHash,
) )
private fun MediaRecord.toDto() = private fun MediaRecord.toDto() =
@ -361,7 +390,7 @@ class BookDtoDao(
status = status, status = status,
mediaType = mediaType ?: "", mediaType = mediaType ?: "",
pagesCount = pageCount.toInt(), pagesCount = pageCount.toInt(),
comment = comment ?: "" comment = comment ?: "",
) )
private fun BookMetadataRecord.toDto(authors: List<AuthorDto>, tags: Set<String>, links: List<WebLinkDto>) = private fun BookMetadataRecord.toDto(authors: List<AuthorDto>, tags: Set<String>, links: List<WebLinkDto>) =
@ -385,7 +414,7 @@ class BookDtoDao(
links = links, links = links,
linksLock = linksLock, linksLock = linksLock,
created = createdDate, created = createdDate,
lastModified = lastModifiedDate lastModified = lastModifiedDate,
) )
private fun ReadProgressRecord.toDto() = private fun ReadProgressRecord.toDto() =
@ -394,6 +423,6 @@ class BookDtoDao(
completed = completed, completed = completed,
readDate = readDate, readDate = readDate,
created = createdDate, created = createdDate,
lastModified = lastModifiedDate lastModified = lastModifiedDate,
) )
} }

View file

@ -39,4 +39,6 @@ interface BookDtoRepository {
): BookDto? ): BookDto?
fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<BookDto> fun findAllOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<BookDto>
fun findAllDuplicates(userId: String, pageable: Pageable): Page<BookDto>
} }

View file

@ -98,7 +98,7 @@ class BookController(
private val readListRepository: ReadListRepository, private val readListRepository: ReadListRepository,
private val contentDetector: ContentDetector, private val contentDetector: ContentDetector,
private val eventPublisher: EventPublisher, private val eventPublisher: EventPublisher,
private val thumbnailBookRepository: ThumbnailBookRepository private val thumbnailBookRepository: ThumbnailBookRepository,
) { ) {
@PageableAsQueryParam @PageableAsQueryParam
@ -112,7 +112,7 @@ class BookController(
@RequestParam(name = "released_after", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) releasedAfter: LocalDate?, @RequestParam(name = "released_after", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) releasedAfter: LocalDate?,
@RequestParam(name = "tag", required = false) tags: List<String>?, @RequestParam(name = "tag", required = false) tags: List<String>?,
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
@Parameter(hidden = true) page: Pageable @Parameter(hidden = true) page: Pageable,
): Page<BookDto> { ): Page<BookDto> {
val sort = val sort =
when { when {
@ -126,7 +126,7 @@ class BookController(
else PageRequest.of( else PageRequest.of(
page.pageNumber, page.pageNumber,
page.pageSize, page.pageSize,
sort sort,
) )
val bookSearch = BookSearchWithReadProgress( val bookSearch = BookSearchWithReadProgress(
@ -135,7 +135,7 @@ class BookController(
mediaStatus = mediaStatus, mediaStatus = mediaStatus,
readStatus = readStatus, readStatus = readStatus,
releasedAfter = releasedAfter, releasedAfter = releasedAfter,
tags = tags tags = tags,
) )
return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest) return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest)
@ -148,7 +148,7 @@ class BookController(
fun getLatestBooks( fun getLatestBooks(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
@Parameter(hidden = true) page: Pageable @Parameter(hidden = true) page: Pageable,
): Page<BookDto> { ): Page<BookDto> {
val sort = Sort.by(Sort.Order.desc("lastModifiedDate")) val sort = Sort.by(Sort.Order.desc("lastModifiedDate"))
@ -157,15 +157,15 @@ class BookController(
else PageRequest.of( else PageRequest.of(
page.pageNumber, page.pageNumber,
page.pageSize, page.pageSize,
sort sort,
) )
return bookDtoRepository.findAll( return bookDtoRepository.findAll(
BookSearchWithReadProgress( BookSearchWithReadProgress(
libraryIds = principal.user.getAuthorizedLibraryIds(null) libraryIds = principal.user.getAuthorizedLibraryIds(null),
), ),
principal.user.id, principal.user.id,
pageRequest pageRequest,
).map { it.restrictUrl(!principal.user.roleAdmin) } ).map { it.restrictUrl(!principal.user.roleAdmin) }
} }
@ -175,18 +175,43 @@ class BookController(
fun getBooksOnDeck( fun getBooksOnDeck(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: List<String>?, @RequestParam(name = "library_id", required = false) libraryIds: List<String>?,
@Parameter(hidden = true) page: Pageable @Parameter(hidden = true) page: Pageable,
): Page<BookDto> = ): Page<BookDto> =
bookDtoRepository.findAllOnDeck( bookDtoRepository.findAllOnDeck(
principal.user.id, principal.user.id,
principal.user.getAuthorizedLibraryIds(libraryIds), principal.user.getAuthorizedLibraryIds(libraryIds),
page page,
).map { it.restrictUrl(!principal.user.roleAdmin) } ).map { it.restrictUrl(!principal.user.roleAdmin) }
@PageableWithoutSortAsQueryParam
@GetMapping("api/v1/books/duplicates")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun getDuplicateBooks(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
@Parameter(hidden = true) page: Pageable,
): Page<BookDto> {
val sort =
when {
page.sort.isSorted -> page.sort
else -> Sort.by(Sort.Order.asc("fileHash"))
}
val pageRequest =
if (unpaged) Pageable.unpaged()
else PageRequest.of(
page.pageNumber,
page.pageSize,
sort,
)
return bookDtoRepository.findAllDuplicates(principal.user.id, pageRequest)
}
@GetMapping("api/v1/books/{bookId}") @GetMapping("api/v1/books/{bookId}")
fun getOneBook( fun getOneBook(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String @PathVariable bookId: String,
): BookDto = ): BookDto =
bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let { bookDtoRepository.findByIdOrNull(bookId, principal.user.id)?.let {
if (!principal.user.canAccessLibrary(it.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessLibrary(it.libraryId)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -196,7 +221,7 @@ class BookController(
@GetMapping("api/v1/books/{bookId}/previous") @GetMapping("api/v1/books/{bookId}/previous")
fun getBookSiblingPrevious( fun getBookSiblingPrevious(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String @PathVariable bookId: String,
): BookDto { ): BookDto {
bookRepository.getLibraryIdOrNull(bookId)?.let { bookRepository.getLibraryIdOrNull(bookId)?.let {
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -210,7 +235,7 @@ class BookController(
@GetMapping("api/v1/books/{bookId}/next") @GetMapping("api/v1/books/{bookId}/next")
fun getBookSiblingNext( fun getBookSiblingNext(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String @PathVariable bookId: String,
): BookDto { ): BookDto {
bookRepository.getLibraryIdOrNull(bookId)?.let { bookRepository.getLibraryIdOrNull(bookId)?.let {
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -224,7 +249,7 @@ class BookController(
@GetMapping("api/v1/books/{bookId}/readlists") @GetMapping("api/v1/books/{bookId}/readlists")
fun getAllReadListsByBook( fun getAllReadListsByBook(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "bookId") bookId: String @PathVariable(name = "bookId") bookId: String,
): List<ReadListDto> { ): List<ReadListDto> {
bookRepository.getLibraryIdOrNull(bookId)?.let { bookRepository.getLibraryIdOrNull(bookId)?.let {
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -238,13 +263,13 @@ class BookController(
@GetMapping( @GetMapping(
value = [ value = [
"api/v1/books/{bookId}/thumbnail", "api/v1/books/{bookId}/thumbnail",
"opds/v1.2/books/{bookId}/thumbnail" "opds/v1.2/books/{bookId}/thumbnail",
], ],
produces = [MediaType.IMAGE_JPEG_VALUE] produces = [MediaType.IMAGE_JPEG_VALUE],
) )
fun getBookThumbnail( fun getBookThumbnail(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String @PathVariable bookId: String,
): ByteArray { ): ByteArray {
bookRepository.getLibraryIdOrNull(bookId)?.let { bookRepository.getLibraryIdOrNull(bookId)?.let {
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -258,7 +283,7 @@ class BookController(
fun getBookThumbnailById( fun getBookThumbnailById(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "bookId") bookId: String, @PathVariable(name = "bookId") bookId: String,
@PathVariable(name = "thumbnailId") thumbnailId: String @PathVariable(name = "thumbnailId") thumbnailId: String,
): ByteArray { ): ByteArray {
bookRepository.getLibraryIdOrNull(bookId)?.let { bookRepository.getLibraryIdOrNull(bookId)?.let {
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -301,9 +326,9 @@ class BookController(
bookId = book.id, bookId = book.id,
thumbnail = file.bytes, thumbnail = file.bytes,
type = ThumbnailBook.Type.USER_UPLOADED, type = ThumbnailBook.Type.USER_UPLOADED,
selected = selected selected = selected,
), ),
if (selected) MarkSelectedPreference.YES else MarkSelectedPreference.NO if (selected) MarkSelectedPreference.YES else MarkSelectedPreference.NO,
) )
} }
@ -351,14 +376,14 @@ class BookController(
value = [ value = [
"api/v1/books/{bookId}/file", "api/v1/books/{bookId}/file",
"api/v1/books/{bookId}/file/*", "api/v1/books/{bookId}/file/*",
"opds/v1.2/books/{bookId}/file/*" "opds/v1.2/books/{bookId}/file/*",
], ],
produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE] produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE],
) )
@PreAuthorize("hasRole('$ROLE_FILE_DOWNLOAD')") @PreAuthorize("hasRole('$ROLE_FILE_DOWNLOAD')")
fun getBookFile( fun getBookFile(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String @PathVariable bookId: String,
): ResponseEntity<StreamingResponseBody> = ): ResponseEntity<StreamingResponseBody> =
bookRepository.findByIdOrNull(bookId)?.let { book -> bookRepository.findByIdOrNull(bookId)?.let { book ->
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -378,7 +403,7 @@ class BookController(
contentDisposition = ContentDisposition.builder("attachment") contentDisposition = ContentDisposition.builder("attachment")
.filename(book.path.name) .filename(book.path.name)
.build() .build()
} },
) )
.contentType(getMediaTypeOrDefault(media.mediaType)) .contentType(getMediaTypeOrDefault(media.mediaType))
.contentLength(this.contentLength()) .contentLength(this.contentLength())
@ -393,7 +418,7 @@ class BookController(
@GetMapping("api/v1/books/{bookId}/pages") @GetMapping("api/v1/books/{bookId}/pages")
fun getBookPages( fun getBookPages(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String @PathVariable bookId: String,
): List<PageDto> = ): List<PageDto> =
bookRepository.findByIdOrNull(bookId)?.let { book -> bookRepository.findByIdOrNull(bookId)?.let { book ->
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -403,7 +428,7 @@ class BookController(
Media.Status.UNKNOWN -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book has not been analyzed yet") Media.Status.UNKNOWN -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book has not been analyzed yet")
Media.Status.OUTDATED -> throw ResponseStatusException( Media.Status.OUTDATED -> throw ResponseStatusException(
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
"Book is outdated and must be re-analyzed" "Book is outdated and must be re-analyzed",
) )
Media.Status.ERROR -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed") Media.Status.ERROR -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
Media.Status.UNSUPPORTED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book format is not supported") Media.Status.UNSUPPORTED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book format is not supported")
@ -417,16 +442,16 @@ class BookController(
content = [ content = [
Content( Content(
mediaType = "image/*", mediaType = "image/*",
schema = Schema(type = "string", format = "binary") schema = Schema(type = "string", format = "binary"),
) ),
] ],
) )
@GetMapping( @GetMapping(
value = [ value = [
"api/v1/books/{bookId}/pages/{pageNumber}", "api/v1/books/{bookId}/pages/{pageNumber}",
"opds/v1.2/books/{bookId}/pages/{pageNumber}" "opds/v1.2/books/{bookId}/pages/{pageNumber}",
], ],
produces = [MediaType.ALL_VALUE] produces = [MediaType.ALL_VALUE],
) )
@PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')") @PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')")
fun getBookPage( fun getBookPage(
@ -436,7 +461,7 @@ class BookController(
@PathVariable pageNumber: Int, @PathVariable pageNumber: Int,
@Parameter( @Parameter(
description = "Convert the image to the provided format.", description = "Convert the image to the provided format.",
schema = Schema(allowableValues = ["jpeg", "png"]) schema = Schema(allowableValues = ["jpeg", "png"]),
) )
@RequestParam(value = "convert", required = false) convertTo: String?, @RequestParam(value = "convert", required = false) convertTo: String?,
@Parameter(description = "If set to true, pages will start at index 0. If set to false, pages will start at index 1.") @Parameter(description = "If set to true, pages will start at index 0. If set to false, pages will start at index 1.")
@ -471,7 +496,7 @@ class BookController(
contentDisposition = ContentDisposition.builder("inline") contentDisposition = ContentDisposition.builder("inline")
.filename(imageFileName) .filename(imageFileName)
.build() .build()
} },
) )
.contentType(getMediaTypeOrDefault(pageContent.mediaType)) .contentType(getMediaTypeOrDefault(pageContent.mediaType))
.setNotModified(media) .setNotModified(media)
@ -491,13 +516,13 @@ class BookController(
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
@GetMapping( @GetMapping(
value = ["api/v1/books/{bookId}/pages/{pageNumber}/thumbnail"], value = ["api/v1/books/{bookId}/pages/{pageNumber}/thumbnail"],
produces = [MediaType.IMAGE_JPEG_VALUE] produces = [MediaType.IMAGE_JPEG_VALUE],
) )
fun getBookPageThumbnail( fun getBookPageThumbnail(
@AuthenticationPrincipal principal: KomgaPrincipal, @AuthenticationPrincipal principal: KomgaPrincipal,
request: WebRequest, request: WebRequest,
@PathVariable bookId: String, @PathVariable bookId: String,
@PathVariable pageNumber: Int @PathVariable pageNumber: Int,
): ResponseEntity<ByteArray> = ): ResponseEntity<ByteArray> =
bookRepository.findByIdOrNull(bookId)?.let { book -> bookRepository.findByIdOrNull(bookId)?.let { book ->
val media = mediaRepository.findById(bookId) val media = mediaRepository.findById(bookId)
@ -591,7 +616,7 @@ class BookController(
@PathVariable bookId: String, @PathVariable bookId: String,
@Parameter(description = "page can be omitted if completed is set to true. completed can be omitted, and will be set accordingly depending on the page passed and the total number of pages in the book.") @Parameter(description = "page can be omitted if completed is set to true. completed can be omitted, and will be set accordingly depending on the page passed and the total number of pages in the book.")
@Valid @RequestBody readProgress: ReadProgressUpdateDto, @Valid @RequestBody readProgress: ReadProgressUpdateDto,
@AuthenticationPrincipal principal: KomgaPrincipal @AuthenticationPrincipal principal: KomgaPrincipal,
) { ) {
bookRepository.findByIdOrNull(bookId)?.let { book -> bookRepository.findByIdOrNull(bookId)?.let { book ->
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -612,7 +637,7 @@ class BookController(
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteReadProgress( fun deleteReadProgress(
@PathVariable bookId: String, @PathVariable bookId: String,
@AuthenticationPrincipal principal: KomgaPrincipal @AuthenticationPrincipal principal: KomgaPrincipal,
) { ) {
bookRepository.findByIdOrNull(bookId)?.let { book -> bookRepository.findByIdOrNull(bookId)?.let { book ->
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN) if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
@ -647,7 +672,7 @@ class BookController(
@PreAuthorize("hasRole('$ROLE_ADMIN')") @PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED) @ResponseStatus(HttpStatus.ACCEPTED)
fun deleteBook( fun deleteBook(
@PathVariable bookId: String @PathVariable bookId: String,
) { ) {
taskReceiver.deleteBook( taskReceiver.deleteBook(
bookId = bookId, bookId = bookId,

View file

@ -25,6 +25,7 @@ data class BookDto(
val metadata: BookMetadataDto, val metadata: BookMetadataDto,
val readProgress: ReadProgressDto? = null, val readProgress: ReadProgressDto? = null,
val deleted: Boolean, val deleted: Boolean,
val fileHash: String,
) )
fun BookDto.restrictUrl(restrict: Boolean) = fun BookDto.restrictUrl(restrict: Boolean) =