From 5777952c05246f8e1ab8408dbd11c130b70ef113 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 26 Jan 2022 18:18:23 +0800 Subject: [PATCH] feat(api): wip version of the page-hashes endpoints --- .../org/gotson/komga/domain/model/PageHash.kt | 20 ++++ .../komga/domain/model/PageHashMatch.kt | 9 ++ .../komga/domain/model/PageHashUnknown.kt | 8 ++ .../domain/persistence/PageHashRepository.kt | 16 +++ .../komga/domain/service/PageHashLifecycle.kt | 22 ++++ .../komga/infrastructure/jooq/PageHashDao.kt | 103 ++++++++++++++++++ .../interfaces/api/rest/PageHashController.kt | 85 +++++++++++++++ .../interfaces/api/rest/dto/PageHashDto.kt | 25 +++++ .../api/rest/dto/PageHashMatchDto.kt | 17 +++ .../api/rest/dto/PageHashUnknownDto.kt | 19 ++++ 10 files changed, 324 insertions(+) create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/PageHash.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashMatch.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashUnknown.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/persistence/PageHashRepository.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/service/PageHashLifecycle.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/PageHashDao.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashDto.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashMatchDto.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashUnknownDto.kt diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHash.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHash.kt new file mode 100644 index 000000000..ad94ed3bb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHash.kt @@ -0,0 +1,20 @@ +package org.gotson.komga.domain.model + +import java.time.LocalDateTime + +data class PageHash( + val hash: String, + val mediaType: String, + val size: Long? = null, + val action: Action, + val deleteCount: Int = 0, + + override val createdDate: LocalDateTime = LocalDateTime.now(), + override val lastModifiedDate: LocalDateTime = createdDate, +) : Auditable() { + enum class Action { + DELETE_AUTO, + DELETE_MANUAL, + IGNORE, + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashMatch.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashMatch.kt new file mode 100644 index 000000000..291146ecb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashMatch.kt @@ -0,0 +1,9 @@ +package org.gotson.komga.domain.model + +import java.net.URL + +data class PageHashMatch( + val bookId: String, + val url: URL, + val pageNumber: Int, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashUnknown.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashUnknown.kt new file mode 100644 index 000000000..eb1d1013d --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/PageHashUnknown.kt @@ -0,0 +1,8 @@ +package org.gotson.komga.domain.model + +data class PageHashUnknown( + val hash: String, + val mediaType: String, + val size: Long? = null, + val matchCount: Int, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/PageHashRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/PageHashRepository.kt new file mode 100644 index 000000000..fc3013899 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/PageHashRepository.kt @@ -0,0 +1,16 @@ +package org.gotson.komga.domain.persistence + +import org.gotson.komga.domain.model.PageHash +import org.gotson.komga.domain.model.PageHashMatch +import org.gotson.komga.domain.model.PageHashUnknown +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable + +interface PageHashRepository { + fun findAllKnown(actions: List?, pageable: Pageable): Page + fun findAllUnknown(pageable: Pageable): Page + + fun findMatchesByHash(hash: String, pageable: Pageable): Page + + fun getKnownThumbnail(hash: String): ByteArray? +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/PageHashLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/PageHashLifecycle.kt new file mode 100644 index 000000000..5eff8b65f --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/PageHashLifecycle.kt @@ -0,0 +1,22 @@ +package org.gotson.komga.domain.service + +import org.gotson.komga.domain.model.BookPageContent +import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.domain.persistence.PageHashRepository +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service + +@Service +class PageHashLifecycle( + private val pageHashRepository: PageHashRepository, + private val bookLifecycle: BookLifecycle, + private val bookRepository: BookRepository, +) { + + fun getPage(hash: String, resizeTo: Int? = null): BookPageContent? { + val match = pageHashRepository.findMatchesByHash(hash, Pageable.ofSize(1)).firstOrNull() ?: return null + val book = bookRepository.findByIdOrNull(match.bookId) ?: return null + + return bookLifecycle.getBookPage(book, match.pageNumber, resizeTo = resizeTo) + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/PageHashDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/PageHashDao.kt new file mode 100644 index 000000000..2ba429474 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/PageHashDao.kt @@ -0,0 +1,103 @@ +package org.gotson.komga.infrastructure.jooq + +import org.gotson.komga.domain.model.PageHash +import org.gotson.komga.domain.model.PageHashMatch +import org.gotson.komga.domain.model.PageHashUnknown +import org.gotson.komga.domain.persistence.PageHashRepository +import org.gotson.komga.jooq.Tables +import org.jooq.DSLContext +import org.jooq.impl.DSL +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Component +import java.net.URL + +@Component +class PageHashDao( + private val dsl: DSLContext, +) : PageHashRepository { + + private val p = Tables.MEDIA_PAGE + private val b = Tables.BOOK + + private val sorts = mapOf( + "hash" to p.FILE_HASH, + "mediatype" to p.MEDIA_TYPE, + "size" to DSL.field("size"), + "matchCount" to DSL.field("count"), + "url" to b.URL, + "bookId" to b.ID, + "pageNumber" to p.NUMBER, + ) + + override fun findAllKnown(actions: List?, pageable: Pageable): Page { + TODO("Not yet implemented") + } + + override fun findAllUnknown(pageable: Pageable): Page { + val query = dsl.select( + p.FILE_HASH, + p.MEDIA_TYPE, + p.FILE_SIZE, + DSL.count(p.BOOK_ID).`as`("count"), + ) + .from(p) + .where(p.FILE_HASH.ne("")) + .groupBy(p.FILE_HASH, p.MEDIA_TYPE, p.FILE_SIZE) + .having(DSL.count(p.BOOK_ID).gt(1)) + + val count = dsl.fetchCount(query) + + val orderBy = pageable.sort.toOrderBy(sorts) + val items = query + .orderBy(orderBy) + .apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } + .fetch { + PageHashUnknown(it.value1(), it.value2(), it.value3(), it.value4()) + } + + val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted() + return PageImpl( + items, + if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort) + else PageRequest.of(0, maxOf(count, 20), pageSort), + count.toLong(), + ) + } + + override fun findMatchesByHash(hash: String, pageable: Pageable): Page { + val query = dsl.select(p.BOOK_ID, b.URL, p.NUMBER) + .from(p) + .leftJoin(b).on(p.BOOK_ID.eq(b.ID)) + .where(p.FILE_HASH.eq(hash)) + + val count = dsl.fetchCount(query) + + val orderBy = pageable.sort.toOrderBy(sorts) + val items = query + .orderBy(orderBy) + .apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } + .fetch { + PageHashMatch( + bookId = it.value1(), + url = URL(it.value2()), + pageNumber = it.value3() + 1, + ) + } + + val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted() + return PageImpl( + items, + if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort) + else PageRequest.of(0, maxOf(count, 20), pageSort), + count.toLong(), + ) + } + + override fun getKnownThumbnail(hash: String): ByteArray? { + TODO("Not yet implemented") + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt new file mode 100644 index 000000000..5ea50fba1 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt @@ -0,0 +1,85 @@ +package org.gotson.komga.interfaces.api.rest + +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.gotson.komga.domain.model.PageHash +import org.gotson.komga.domain.model.ROLE_ADMIN +import org.gotson.komga.domain.persistence.PageHashRepository +import org.gotson.komga.domain.service.PageHashLifecycle +import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam +import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault +import org.gotson.komga.interfaces.api.rest.dto.PageHashDto +import org.gotson.komga.interfaces.api.rest.dto.PageHashMatchDto +import org.gotson.komga.interfaces.api.rest.dto.PageHashUnknownDto +import org.gotson.komga.interfaces.api.rest.dto.toDto +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +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.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException + +@RestController +@RequestMapping("api/v1/page-hashes", produces = [MediaType.APPLICATION_JSON_VALUE]) +@PreAuthorize("hasRole('$ROLE_ADMIN')") +class PageHashController( + private val pageHashRepository: PageHashRepository, + private val pageHashLifecycle: PageHashLifecycle, +) { + + @GetMapping + @PageableAsQueryParam + fun getPageHashes( + @RequestParam(name = "action", required = false) actions: List?, + @Parameter(hidden = true) page: Pageable, + ): Page = + pageHashRepository.findAllKnown(actions, page).map { it.toDto() } + + @GetMapping("/{hash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE],) + @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) + fun getPageHashThumbnail(@PathVariable hash: String): ByteArray = + pageHashRepository.getKnownThumbnail(hash) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + @GetMapping("/unknown") + @PageableAsQueryParam + fun getUnknownPageHashes( + @Parameter(hidden = true) page: Pageable, + ): Page = + pageHashRepository.findAllUnknown(page).map { it.toDto() } + + @GetMapping("unknown/{hash}") + @PageableAsQueryParam + fun getUnknownPageHashMatches( + @PathVariable hash: String, + @Parameter(hidden = true) page: Pageable, + ): Page = + pageHashRepository.findMatchesByHash(hash, page).map { it.toDto() } + + @GetMapping("unknown/{hash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE],) + @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) + fun getUnknownPageHashThumbnail( + @PathVariable hash: String, + @RequestParam("resize") resize: Int? = null, + ): ResponseEntity = + pageHashLifecycle.getPage(hash, resize)?.let { + ResponseEntity.ok() + .contentType(getMediaTypeOrDefault(it.mediaType)) + .body(it.content) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + @PutMapping + @ResponseStatus(HttpStatus.ACCEPTED) + fun updatePageHash() { + TODO() + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashDto.kt new file mode 100644 index 000000000..3c971df3a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashDto.kt @@ -0,0 +1,25 @@ +package org.gotson.komga.interfaces.api.rest.dto + +import org.gotson.komga.domain.model.PageHash +import java.time.LocalDateTime + +data class PageHashDto( + val hash: String, + val mediaType: String, + val size: Long?, + val action: PageHash.Action, + val deleteCount: Int, + + val created: LocalDateTime, + val lastModified: LocalDateTime, +) + +fun PageHash.toDto() = PageHashDto( + hash = hash, + mediaType = mediaType, + size = size, + action = action, + deleteCount = deleteCount, + created = createdDate, + lastModified = lastModifiedDate, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashMatchDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashMatchDto.kt new file mode 100644 index 000000000..4c30c2dda --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashMatchDto.kt @@ -0,0 +1,17 @@ +package org.gotson.komga.interfaces.api.rest.dto + +import org.gotson.komga.domain.model.PageHashMatch +import org.gotson.komga.infrastructure.web.toFilePath + +data class PageHashMatchDto( + val bookId: String, + val url: String, + val pageNumber: Int, +) + +fun PageHashMatch.toDto() = + PageHashMatchDto( + bookId = bookId, + url = url.toFilePath(), + pageNumber = pageNumber, + ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashUnknownDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashUnknownDto.kt new file mode 100644 index 000000000..b3defbe7c --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageHashUnknownDto.kt @@ -0,0 +1,19 @@ +package org.gotson.komga.interfaces.api.rest.dto + +import com.jakewharton.byteunits.BinaryByteUnit +import org.gotson.komga.domain.model.PageHashUnknown + +data class PageHashUnknownDto( + val hash: String, + val mediaType: String, + val sizeBytes: Long?, + val size: String? = sizeBytes?.let { BinaryByteUnit.format(it) }, + val matchCount: Int, +) + +fun PageHashUnknown.toDto() = PageHashUnknownDto( + hash = hash, + mediaType = mediaType, + sizeBytes = size, + matchCount = matchCount, +)