diff --git a/komga-webui/src/components/ItemCard.vue b/komga-webui/src/components/ItemCard.vue index fb2c8bb53..908994244 100644 --- a/komga-webui/src/components/ItemCard.vue +++ b/komga-webui/src/components/ItemCard.vue @@ -267,7 +267,7 @@ export default Vue.extend({ } }, thumbnailSeriesAdded(event: ThumbnailSeriesSseDto) { - if (this.thumbnailError && (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id)) { + if (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id) { this.thumbnailCacheBust = '?' + this.$_.random(1000) } }, diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20210902100930__series_thumbnail_blob.sql b/komga/src/flyway/resources/db/migration/sqlite/V20210902100930__series_thumbnail_blob.sql new file mode 100644 index 000000000..e3a69b810 --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20210902100930__series_thumbnail_blob.sql @@ -0,0 +1,20 @@ +ALTER TABLE THUMBNAIL_SERIES RENAME TO TMP_THUMBNAIL_SERIES; + +CREATE TABLE THUMBNAIL_SERIES +( + ID varchar NOT NULL PRIMARY KEY, + URL varchar NULL DEFAULT NULL, + SELECTED boolean NOT NULL DEFAULT 0, + THUMBNAIL blob NULL DEFAULT NULL, + TYPE varchar not null, + CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + SERIES_ID varchar NOT NULL, + FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID) +); + +INSERT INTO THUMBNAIL_SERIES(ID, URL, SELECTED, CREATED_DATE, LAST_MODIFIED_DATE, SERIES_ID, TYPE) +SELECT ID, URL, SELECTED, CREATED_DATE, LAST_MODIFIED_DATE, SERIES_ID, "SIDECAR" AS TYPE +FROM TMP_THUMBNAIL_SERIES; + +DROP TABLE TMP_THUMBNAIL_SERIES; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/MarkSelectedPreference.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/MarkSelectedPreference.kt new file mode 100644 index 000000000..ccf51a3c1 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/MarkSelectedPreference.kt @@ -0,0 +1,5 @@ +package org.gotson.komga.domain.model + +enum class MarkSelectedPreference { + NO, YES, IF_NONE_EXIST +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt index 9ef2b6fab..0ae66feb7 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt @@ -3,6 +3,8 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator import java.io.Serializable import java.net.URL +import java.nio.file.Files +import java.nio.file.Paths import java.time.LocalDateTime data class ThumbnailBook( @@ -51,4 +53,9 @@ data class ThumbnailBook( result = 31 * result + lastModifiedDate.hashCode() return result } + + fun exists(): Boolean { + if (url != null) return Files.exists(Paths.get(url.toURI())) + return thumbnail != null + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt index e9dc316fc..4f9062648 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt @@ -3,15 +3,59 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator import java.io.Serializable import java.net.URL +import java.nio.file.Files +import java.nio.file.Paths import java.time.LocalDateTime data class ThumbnailSeries( - val url: URL, + val thumbnail: ByteArray? = null, + val url: URL? = null, val selected: Boolean = false, + val type: Type, val id: String = TsidCreator.getTsid256().toString(), val seriesId: String = "", override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable(), Serializable +) : Auditable(), Serializable { + enum class Type { + SIDECAR, USER_UPLOADED + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ThumbnailSeries) return false + + if (thumbnail != null) { + if (other.thumbnail == null) return false + if (!thumbnail.contentEquals(other.thumbnail)) return false + } else if (other.thumbnail != null) return false + if (url != other.url) return false + if (selected != other.selected) return false + if (type != other.type) return false + if (id != other.id) return false + if (seriesId != other.seriesId) return false + if (createdDate != other.createdDate) return false + if (lastModifiedDate != other.lastModifiedDate) return false + + return true + } + + override fun hashCode(): Int { + var result = thumbnail?.contentHashCode() ?: 0 + result = 31 * result + (url?.hashCode() ?: 0) + result = 31 * result + selected.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + seriesId.hashCode() + result = 31 * result + createdDate.hashCode() + result = 31 * result + lastModifiedDate.hashCode() + return result + } + + fun exists(): Boolean { + if (url != null) return Files.exists(Paths.get(url.toURI())) + return thumbnail != null + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ThumbnailSeriesRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ThumbnailSeriesRepository.kt index 9394796f2..6e3666ee4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ThumbnailSeriesRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ThumbnailSeriesRepository.kt @@ -3,6 +3,8 @@ package org.gotson.komga.domain.persistence import org.gotson.komga.domain.model.ThumbnailSeries interface ThumbnailSeriesRepository { + fun findByIdOrNull(thumbnailId: String): ThumbnailSeries? + fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries? fun findAllBySeriesId(seriesId: String): Collection diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt index b9c6912d7..4e5f00bc2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt @@ -25,8 +25,6 @@ import org.gotson.komga.infrastructure.image.ImageType import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionTemplate import java.io.File -import java.nio.file.Files -import java.nio.file.Paths import java.time.LocalDateTime private val logger = KotlinLogging.logger {} @@ -161,15 +159,6 @@ class BookLifecycle( } } - private fun ThumbnailBook.exists(): Boolean { - if (type == ThumbnailBook.Type.SIDECAR) { - if (url != null) - return Files.exists(Paths.get(url.toURI())) - return false - } - return true - } - @Throws( ImageConversionException::class, MediaNotReadyException::class, diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/LocalArtworkLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/LocalArtworkLifecycle.kt index 57e3fd36b..b68049f2f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/LocalArtworkLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/LocalArtworkLifecycle.kt @@ -2,6 +2,7 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import org.gotson.komga.domain.model.Book +import org.gotson.komga.domain.model.MarkSelectedPreference import org.gotson.komga.domain.model.Series import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.infrastructure.metadata.localartwork.LocalArtworkProvider @@ -35,7 +36,7 @@ class LocalArtworkLifecycle( if (library.importLocalArtwork) localArtworkProvider.getSeriesThumbnails(series).forEach { - seriesLifecycle.addThumbnailForSeries(it) + seriesLifecycle.addThumbnailForSeries(it, if (it.selected) MarkSelectedPreference.IF_NONE_EXIST else MarkSelectedPreference.NO) } else logger.info { "Library is not set to import local artwork, skipping" } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt index 8b6b94d73..802b8b1c6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt @@ -12,6 +12,7 @@ import org.gotson.komga.domain.model.BookMetadataPatchCapability import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.KomgaUser import org.gotson.komga.domain.model.Library +import org.gotson.komga.domain.model.MarkSelectedPreference import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.ReadProgress import org.gotson.komga.domain.model.Series @@ -30,8 +31,6 @@ import org.gotson.komga.domain.persistence.ThumbnailSeriesRepository import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionTemplate import java.io.File -import java.nio.file.Files -import java.nio.file.Paths import java.time.LocalDateTime private val logger = KotlinLogging.logger {} @@ -194,10 +193,10 @@ class SeriesLifecycle( eventPublisher.publishEvent(DomainEvent.ReadProgressSeriesDeleted(seriesId, user.id)) } - fun getThumbnail(seriesId: String): ThumbnailSeries? { + fun getSelectedThumbnail(seriesId: String): ThumbnailSeries? { val selected = thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(seriesId) - if (selected == null || !selected.exists()) { + if (selected == null || (selected.type == ThumbnailSeries.Type.SIDECAR && !selected.exists())) { thumbnailsHouseKeeping(seriesId) return thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(seriesId) } @@ -205,9 +204,21 @@ class SeriesLifecycle( return selected } + private fun getBytesFromThumbnailSeries(thumbnail: ThumbnailSeries): ByteArray? = + when { + thumbnail.thumbnail != null -> thumbnail.thumbnail + thumbnail.url != null -> File(thumbnail.url.toURI()).readBytes() + else -> null + } + + fun getThumbnailBytesByThumbnailId(thumbnailId: String): ByteArray? = + thumbnailsSeriesRepository.findByIdOrNull(thumbnailId)?.let { + getBytesFromThumbnailSeries(it) + } + fun getThumbnailBytes(seriesId: String, userId: String): ByteArray? { - getThumbnail(seriesId)?.let { - return File(it.url.toURI()).readBytes() + getSelectedThumbnail(seriesId)?.let { + return getBytesFromThumbnailSeries(it) } seriesRepository.findByIdOrNull(seriesId)?.let { series -> @@ -225,19 +236,31 @@ class SeriesLifecycle( return null } - fun addThumbnailForSeries(thumbnail: ThumbnailSeries) { + fun addThumbnailForSeries(thumbnail: ThumbnailSeries, markSelected: MarkSelectedPreference) { // delete existing thumbnail with the same url - thumbnailsSeriesRepository.findAllBySeriesId(thumbnail.seriesId) - .filter { it.url == thumbnail.url } - .forEach { - thumbnailsSeriesRepository.delete(it.id) - } - thumbnailsSeriesRepository.insert(thumbnail) + if (thumbnail.url != null) { + thumbnailsSeriesRepository.findAllBySeriesId(thumbnail.seriesId) + .filter { it.url == thumbnail.url } + .forEach { + thumbnailsSeriesRepository.delete(it.id) + } + } + thumbnailsSeriesRepository.insert(thumbnail.copy(selected = false)) - eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail)) - - if (thumbnail.selected) + if (markSelected == MarkSelectedPreference.YES || + ( + markSelected == MarkSelectedPreference.IF_NONE_EXIST && + thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null + ) + ) { thumbnailsSeriesRepository.markSelected(thumbnail) + eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail)) + } + } + + fun deleteThumbnailForSeries(thumbnail: ThumbnailSeries) { + require(thumbnail.type == ThumbnailSeries.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" } + thumbnailsSeriesRepository.delete(thumbnail.id) } private fun thumbnailsHouseKeeping(seriesId: String) { @@ -263,6 +286,4 @@ class SeriesLifecycle( } } } - - private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI())) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ThumbnailSeriesDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ThumbnailSeriesDao.kt index 147e672f6..4efa34e65 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ThumbnailSeriesDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ThumbnailSeriesDao.kt @@ -15,6 +15,12 @@ class ThumbnailSeriesDao( ) : ThumbnailSeriesRepository { private val ts = Tables.THUMBNAIL_SERIES + override fun findByIdOrNull(thumbnailId: String): ThumbnailSeries? = + dsl.selectFrom(ts) + .where(ts.ID.eq(thumbnailId)) + .fetchOneInto(ts) + ?.toDomain() + override fun findAllBySeriesId(seriesId: String): Collection = dsl.selectFrom(ts) .where(ts.SERIES_ID.eq(seriesId)) @@ -34,7 +40,9 @@ class ThumbnailSeriesDao( dsl.insertInto(ts) .set(ts.ID, thumbnail.id) .set(ts.SERIES_ID, thumbnail.seriesId) - .set(ts.URL, thumbnail.url.toString()) + .set(ts.URL, thumbnail.url?.toString()) + .set(ts.THUMBNAIL, thumbnail.thumbnail) + .set(ts.TYPE, thumbnail.type.toString()) .set(ts.SELECTED, thumbnail.selected) .execute() } @@ -68,8 +76,10 @@ class ThumbnailSeriesDao( private fun ThumbnailSeriesRecord.toDomain() = ThumbnailSeries( - url = URL(url), + thumbnail = thumbnail, + url = url?.let { URL(it) }, selected = selected, + type = ThumbnailSeries.Type.valueOf(type), id = id, seriesId = seriesId, createdDate = createdDate, diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/localartwork/LocalArtworkProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/localartwork/LocalArtworkProvider.kt index 72bcedfd5..83120ada6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/localartwork/LocalArtworkProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/localartwork/LocalArtworkProvider.kt @@ -65,7 +65,8 @@ class LocalArtworkProvider( ThumbnailSeries( url = path.toUri().toURL(), seriesId = series.id, - selected = index == 0 + selected = index == 0, + type = ThumbnailSeries.Type.SIDECAR ) }.toList() } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt index 0121b1968..f9e7eca0f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt @@ -17,6 +17,7 @@ import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.DomainEvent +import org.gotson.komga.domain.model.MarkSelectedPreference import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.ROLE_ADMIN import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD @@ -24,13 +25,16 @@ import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.SeriesSearch import org.gotson.komga.domain.model.SeriesSearchWithReadProgress +import org.gotson.komga.domain.model.ThumbnailSeries import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.SeriesCollectionRepository import org.gotson.komga.domain.persistence.SeriesMetadataRepository import org.gotson.komga.domain.persistence.SeriesRepository +import org.gotson.komga.domain.persistence.ThumbnailSeriesRepository import org.gotson.komga.domain.service.BookLifecycle import org.gotson.komga.domain.service.SeriesLifecycle import org.gotson.komga.infrastructure.jooq.UnpagedSorted +import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam @@ -42,6 +46,7 @@ import org.gotson.komga.interfaces.rest.dto.CollectionDto import org.gotson.komga.interfaces.rest.dto.GroupCountDto import org.gotson.komga.interfaces.rest.dto.SeriesDto import org.gotson.komga.interfaces.rest.dto.SeriesMetadataUpdateDto +import org.gotson.komga.interfaces.rest.dto.SeriesThumbnailDto import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressDto import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressUpdateDto import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressUpdateV2Dto @@ -74,6 +79,7 @@ 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.multipart.MultipartFile import org.springframework.web.server.ResponseStatusException import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import java.io.OutputStream @@ -96,6 +102,8 @@ class SeriesController( private val collectionRepository: SeriesCollectionRepository, private val readProgressDtoRepository: ReadProgressDtoRepository, private val eventPublisher: EventPublisher, + private val contentDetector: ContentDetector, + private val thumbnailsSeriesRepository: ThumbnailSeriesRepository, ) { @PageableAsQueryParam @@ -322,9 +330,9 @@ class SeriesController( @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["v1/series/{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE]) - fun getSeriesThumbnail( + fun getSeriesDefaultThumbnail( @AuthenticationPrincipal principal: KomgaPrincipal, - @PathVariable(name = "seriesId") seriesId: String + @PathVariable(name = "seriesId") seriesId: String, ): ByteArray { seriesRepository.getLibraryId(seriesId)?.let { if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) @@ -334,6 +342,85 @@ class SeriesController( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) + @GetMapping(value = ["v1/series/{seriesId}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE]) + fun getSeriesThumbnailById( + @AuthenticationPrincipal principal: KomgaPrincipal, + @PathVariable(name = "seriesId") seriesId: String, + @PathVariable(name = "thumbnailId") thumbnailId: String + ): ByteArray { + seriesRepository.getLibraryId(seriesId)?.let { + if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + return seriesLifecycle.getThumbnailBytesByThumbnailId(thumbnailId) + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + @GetMapping(value = ["v1/series/{seriesId}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun getSeriesThumbnails( + @AuthenticationPrincipal principal: KomgaPrincipal, + @PathVariable(name = "seriesId") seriesId: String + ): Collection { + seriesRepository.getLibraryId(seriesId)?.let { + if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + return thumbnailsSeriesRepository.findAllBySeriesId(seriesId) + .map { it.toDto() } + } + + @PostMapping(value = ["v1/series/{seriesId}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) + @PreAuthorize("hasRole('$ROLE_ADMIN')") + @ResponseStatus(HttpStatus.ACCEPTED) + fun postUserUploadedSeriesThumbnail( + @PathVariable(name = "seriesId") seriesId: String, + @RequestParam("file") file: MultipartFile + ) { + val series = seriesRepository.findByIdOrNull(seriesId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + if (!contentDetector.isImage(file.inputStream.buffered().use { contentDetector.detectMediaType(it) })) + throw ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + seriesLifecycle.addThumbnailForSeries( + ThumbnailSeries( + seriesId = series.id, + thumbnail = file.bytes, + type = ThumbnailSeries.Type.USER_UPLOADED + ), + MarkSelectedPreference.YES + ) + } + + @PutMapping("v1/series/{seriesId}/thumbnails/{thumbnailId}/selected") + @PreAuthorize("hasRole('$ROLE_ADMIN')") + @ResponseStatus(HttpStatus.ACCEPTED) + fun postMarkSelectedSeriesThumbnail( + @PathVariable(name = "seriesId") seriesId: String, + @PathVariable(name = "thumbnailId") thumbnailId: String, + ) { + seriesRepository.findByIdOrNull(seriesId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + thumbnailsSeriesRepository.findByIdOrNull(thumbnailId)?.let { + thumbnailsSeriesRepository.markSelected(it) + eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(it)) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + @DeleteMapping("v1/series/{seriesId}/thumbnails/{thumbnailId}") + @PreAuthorize("hasRole('$ROLE_ADMIN')") + @ResponseStatus(HttpStatus.ACCEPTED) + fun deleteUserUploadedSeriesThumbnail( + @PathVariable(name = "seriesId") seriesId: String, + @PathVariable(name = "thumbnailId") thumbnailId: String, + ) { + seriesRepository.findByIdOrNull(seriesId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + thumbnailsSeriesRepository.findByIdOrNull(thumbnailId)?.let { + try { + seriesLifecycle.deleteThumbnailForSeries(it) + } catch (e: IllegalArgumentException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) + } + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + @PageableAsQueryParam @AuthorsAsQueryParam @GetMapping("v1/series/{seriesId}/books") diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesThumbnailDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesThumbnailDto.kt new file mode 100644 index 000000000..a405156d3 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesThumbnailDto.kt @@ -0,0 +1,18 @@ +package org.gotson.komga.interfaces.rest.dto + +import org.gotson.komga.domain.model.ThumbnailSeries + +data class SeriesThumbnailDto( + val id: String, + val seriesId: String, + val type: String, + val selected: Boolean +) + +fun ThumbnailSeries.toDto() = + SeriesThumbnailDto( + id = id, + seriesId = seriesId, + type = type.toString(), + selected = selected + ) diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt index 1dbb302d0..7ab521492 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt @@ -6,6 +6,7 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.gotson.komga.domain.model.BookMetadata import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.ThumbnailSeries import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.domain.model.makeSeries @@ -253,4 +254,13 @@ class SeriesLifecycleTest( assertThat(bookRepository.count()).isEqualTo(0) } } + + @Test + fun `given a sidecar thumbnail when deleting then IllegarlArgumentException is thrown`() { + val thumbnail = ThumbnailSeries(type = ThumbnailSeries.Type.SIDECAR) + + val thrown = catchThrowable { seriesLifecycle.deleteThumbnailForSeries(thumbnail) } + + assertThat(thrown).isInstanceOf(IllegalArgumentException::class.java) + } }