mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 07:23:34 +01:00
feat(api): support custom covers for series
Co-authored-by: Gauthier <gotson@users.noreply.github.com> Co-authored-by: Gauthier Roebroeck <gauthier.roebroeck@gmail.com>
This commit is contained in:
parent
1ba6822fd5
commit
d7470dd7db
14 changed files with 253 additions and 38 deletions
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
enum class MarkSelectedPreference {
|
||||
NO, YES, IF_NONE_EXIST
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ThumbnailSeries>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ThumbnailSeries> =
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SeriesThumbnailDto> {
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue