mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
feat(api): cover upload for books, read lists and collections
Co-authored-by: Gauthier Roebroeck <gauthier.roebroeck@gmail.com>
This commit is contained in:
parent
ff358da598
commit
31ad351144
29 changed files with 894 additions and 31 deletions
|
|
@ -0,0 +1,23 @@
|
||||||
|
CREATE TABLE THUMBNAIL_COLLECTION
|
||||||
|
(
|
||||||
|
ID varchar NOT NULL PRIMARY KEY,
|
||||||
|
SELECTED boolean NOT NULL DEFAULT 0,
|
||||||
|
THUMBNAIL blob NOT NULL,
|
||||||
|
TYPE varchar NOT NULL,
|
||||||
|
COLLECTION_ID varchar NOT NULL,
|
||||||
|
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (COLLECTION_ID) REFERENCES COLLECTION (ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE THUMBNAIL_READLIST
|
||||||
|
(
|
||||||
|
ID varchar NOT NULL PRIMARY KEY,
|
||||||
|
SELECTED boolean NOT NULL DEFAULT 0,
|
||||||
|
THUMBNAIL blob NOT NULL,
|
||||||
|
TYPE varchar NOT NULL,
|
||||||
|
READLIST_ID varchar NOT NULL,
|
||||||
|
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (READLIST_ID) REFERENCES READLIST (ID)
|
||||||
|
);
|
||||||
|
|
@ -32,5 +32,14 @@ sealed class DomainEvent : Serializable {
|
||||||
data class ReadProgressSeriesDeleted(val seriesId: String, val userId: String) : DomainEvent()
|
data class ReadProgressSeriesDeleted(val seriesId: String, val userId: String) : DomainEvent()
|
||||||
|
|
||||||
data class ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
|
data class ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
|
||||||
|
data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : DomainEvent()
|
||||||
|
|
||||||
data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent()
|
data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent()
|
||||||
|
data class ThumbnailSeriesDeleted(val thumbnail: ThumbnailSeries) : DomainEvent()
|
||||||
|
|
||||||
|
data class ThumbnailSeriesCollectionAdded(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
|
||||||
|
data class ThumbnailSeriesCollectionDeleted(val thumbnail: ThumbnailSeriesCollection) : DomainEvent()
|
||||||
|
|
||||||
|
data class ThumbnailReadListAdded(val thumbnail: ThumbnailReadList) : DomainEvent()
|
||||||
|
data class ThumbnailReadListDeleted(val thumbnail: ThumbnailReadList) : DomainEvent()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
enum class MarkSelectedPreference {
|
enum class MarkSelectedPreference {
|
||||||
NO, YES, IF_NONE_EXIST
|
NO, YES, IF_NONE_OR_GENERATED
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ data class ThumbnailBook(
|
||||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||||
) : Auditable(), Serializable {
|
) : Auditable(), Serializable {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
GENERATED, SIDECAR
|
GENERATED, SIDECAR, USER_UPLOADED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class ThumbnailReadList(
|
||||||
|
val thumbnail: ByteArray,
|
||||||
|
val selected: Boolean = false,
|
||||||
|
val type: Type,
|
||||||
|
|
||||||
|
val id: String = TsidCreator.getTsid256().toString(),
|
||||||
|
val readListId: String = "",
|
||||||
|
|
||||||
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
|
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||||
|
) : Auditable(), Serializable {
|
||||||
|
enum class Type {
|
||||||
|
USER_UPLOADED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is ThumbnailReadList) return false
|
||||||
|
|
||||||
|
if (!thumbnail.contentEquals(other.thumbnail)) return false
|
||||||
|
if (selected != other.selected) return false
|
||||||
|
if (type != other.type) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (readListId != other.readListId) return false
|
||||||
|
if (createdDate != other.createdDate) return false
|
||||||
|
if (lastModifiedDate != other.lastModifiedDate) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = thumbnail.contentHashCode()
|
||||||
|
result = 31 * result + selected.hashCode()
|
||||||
|
result = 31 * result + type.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
result = 31 * result + readListId.hashCode()
|
||||||
|
result = 31 * result + createdDate.hashCode()
|
||||||
|
result = 31 * result + lastModifiedDate.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class ThumbnailSeriesCollection(
|
||||||
|
val thumbnail: ByteArray,
|
||||||
|
val selected: Boolean = false,
|
||||||
|
val type: Type,
|
||||||
|
|
||||||
|
val id: String = TsidCreator.getTsid256().toString(),
|
||||||
|
val collectionId: String = "",
|
||||||
|
|
||||||
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
|
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||||
|
) : Auditable(), Serializable {
|
||||||
|
enum class Type {
|
||||||
|
USER_UPLOADED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is ThumbnailSeriesCollection) return false
|
||||||
|
|
||||||
|
if (!thumbnail.contentEquals(other.thumbnail)) return false
|
||||||
|
if (selected != other.selected) return false
|
||||||
|
if (type != other.type) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (collectionId != other.collectionId) return false
|
||||||
|
if (createdDate != other.createdDate) return false
|
||||||
|
if (lastModifiedDate != other.lastModifiedDate) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = thumbnail.contentHashCode()
|
||||||
|
result = 31 * result + selected.hashCode()
|
||||||
|
result = 31 * result + type.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
result = 31 * result + collectionId.hashCode()
|
||||||
|
result = 31 * result + createdDate.hashCode()
|
||||||
|
result = 31 * result + lastModifiedDate.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ package org.gotson.komga.domain.persistence
|
||||||
import org.gotson.komga.domain.model.ThumbnailBook
|
import org.gotson.komga.domain.model.ThumbnailBook
|
||||||
|
|
||||||
interface ThumbnailBookRepository {
|
interface ThumbnailBookRepository {
|
||||||
|
fun findByIdOrNull(thumbnailId: String): ThumbnailBook?
|
||||||
|
|
||||||
fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook?
|
fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook?
|
||||||
|
|
||||||
fun findAllByBookId(bookId: String): Collection<ThumbnailBook>
|
fun findAllByBookId(bookId: String): Collection<ThumbnailBook>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.gotson.komga.domain.persistence
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailReadList
|
||||||
|
|
||||||
|
interface ThumbnailReadListRepository {
|
||||||
|
fun findByIdOrNull(thumbnailId: String): ThumbnailReadList?
|
||||||
|
fun findSelectedByReadListIdOrNull(readListId: String): ThumbnailReadList?
|
||||||
|
fun findAllByReadListId(readListId: String): Collection<ThumbnailReadList>
|
||||||
|
|
||||||
|
fun insert(thumbnail: ThumbnailReadList)
|
||||||
|
fun update(thumbnail: ThumbnailReadList)
|
||||||
|
fun markSelected(thumbnail: ThumbnailReadList)
|
||||||
|
|
||||||
|
fun delete(thumbnailReadListId: String)
|
||||||
|
fun deleteByReadListId(readListId: String)
|
||||||
|
fun deleteByReadListIds(readListIds: Collection<String>)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.gotson.komga.domain.persistence
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
|
||||||
|
|
||||||
|
interface ThumbnailSeriesCollectionRepository {
|
||||||
|
fun findByIdOrNull(thumbnailId: String): ThumbnailSeriesCollection?
|
||||||
|
fun findSelectedByCollectionIdOrNull(collectionId: String): ThumbnailSeriesCollection?
|
||||||
|
fun findAllByCollectionId(collectionId: String): Collection<ThumbnailSeriesCollection>
|
||||||
|
|
||||||
|
fun insert(thumbnail: ThumbnailSeriesCollection)
|
||||||
|
fun update(thumbnail: ThumbnailSeriesCollection)
|
||||||
|
fun markSelected(thumbnail: ThumbnailSeriesCollection)
|
||||||
|
|
||||||
|
fun delete(thumbnailCollectionId: String)
|
||||||
|
fun deleteByCollectionId(collectionId: String)
|
||||||
|
fun deleteByCollectionIds(collectionIds: Collection<String>)
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import org.gotson.komga.domain.model.BookWithMedia
|
||||||
import org.gotson.komga.domain.model.DomainEvent
|
import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.ImageConversionException
|
import org.gotson.komga.domain.model.ImageConversionException
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
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.Media
|
||||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||||
import org.gotson.komga.domain.model.ReadProgress
|
import org.gotson.komga.domain.model.ReadProgress
|
||||||
|
|
@ -82,18 +83,18 @@ class BookLifecycle(
|
||||||
fun generateThumbnailAndPersist(book: Book) {
|
fun generateThumbnailAndPersist(book: Book) {
|
||||||
logger.info { "Generate thumbnail and persist for book: $book" }
|
logger.info { "Generate thumbnail and persist for book: $book" }
|
||||||
try {
|
try {
|
||||||
addThumbnailForBook(bookAnalyzer.generateThumbnail(BookWithMedia(book, mediaRepository.findById(book.id))))
|
addThumbnailForBook(bookAnalyzer.generateThumbnail(BookWithMedia(book, mediaRepository.findById(book.id))), MarkSelectedPreference.IF_NONE_OR_GENERATED)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
logger.error(ex) { "Error while creating thumbnail" }
|
logger.error(ex) { "Error while creating thumbnail" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addThumbnailForBook(thumbnail: ThumbnailBook) {
|
fun addThumbnailForBook(thumbnail: ThumbnailBook, markSelected: MarkSelectedPreference) {
|
||||||
when (thumbnail.type) {
|
when (thumbnail.type) {
|
||||||
ThumbnailBook.Type.GENERATED -> {
|
ThumbnailBook.Type.GENERATED -> {
|
||||||
// only one generated thumbnail is allowed
|
// only one generated thumbnail is allowed
|
||||||
thumbnailBookRepository.deleteByBookIdAndType(thumbnail.bookId, ThumbnailBook.Type.GENERATED)
|
thumbnailBookRepository.deleteByBookIdAndType(thumbnail.bookId, ThumbnailBook.Type.GENERATED)
|
||||||
thumbnailBookRepository.insert(thumbnail)
|
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
|
||||||
}
|
}
|
||||||
ThumbnailBook.Type.SIDECAR -> {
|
ThumbnailBook.Type.SIDECAR -> {
|
||||||
// delete existing thumbnail with the same url
|
// delete existing thumbnail with the same url
|
||||||
|
|
@ -102,16 +103,37 @@ class BookLifecycle(
|
||||||
.forEach {
|
.forEach {
|
||||||
thumbnailBookRepository.delete(it.id)
|
thumbnailBookRepository.delete(it.id)
|
||||||
}
|
}
|
||||||
thumbnailBookRepository.insert(thumbnail)
|
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
|
||||||
|
}
|
||||||
|
ThumbnailBook.Type.USER_UPLOADED -> {
|
||||||
|
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (markSelected) {
|
||||||
|
MarkSelectedPreference.YES -> {
|
||||||
|
thumbnailBookRepository.markSelected(thumbnail)
|
||||||
|
}
|
||||||
|
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
|
||||||
|
val selectedThumbnail = thumbnailBookRepository.findSelectedByBookIdOrNull(thumbnail.bookId)
|
||||||
|
|
||||||
|
if (selectedThumbnail == null || selectedThumbnail.type == ThumbnailBook.Type.GENERATED)
|
||||||
|
thumbnailBookRepository.markSelected(thumbnail)
|
||||||
|
else thumbnailsHouseKeeping(thumbnail.bookId)
|
||||||
|
}
|
||||||
|
MarkSelectedPreference.NO -> {
|
||||||
|
thumbnailsHouseKeeping(thumbnail.bookId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail))
|
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
if (thumbnail.selected)
|
fun deleteThumbnailForBook(thumbnail: ThumbnailBook) {
|
||||||
thumbnailBookRepository.markSelected(thumbnail)
|
require(thumbnail.type == ThumbnailBook.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
|
||||||
else
|
thumbnailBookRepository.delete(thumbnail.id)
|
||||||
thumbnailsHouseKeeping(thumbnail.bookId)
|
thumbnailsHouseKeeping(thumbnail.bookId)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailBookDeleted(thumbnail))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnail(bookId: String): ThumbnailBook? {
|
fun getThumbnail(bookId: String): ThumbnailBook? {
|
||||||
|
|
@ -136,6 +158,18 @@ class BookLifecycle(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getThumbnailBytesByThumbnailId(thumbnailId: String): ByteArray? =
|
||||||
|
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
getBytesFromThumbnailBook(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBytesFromThumbnailBook(thumbnail: ThumbnailBook): ByteArray? =
|
||||||
|
when {
|
||||||
|
thumbnail.thumbnail != null -> thumbnail.thumbnail
|
||||||
|
thumbnail.url != null -> File(thumbnail.url.toURI()).readBytes()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
private fun thumbnailsHouseKeeping(bookId: String) {
|
private fun thumbnailsHouseKeeping(bookId: String) {
|
||||||
logger.info { "House keeping thumbnails for book: $bookId" }
|
logger.info { "House keeping thumbnails for book: $bookId" }
|
||||||
val all = thumbnailBookRepository.findAllByBookId(bookId)
|
val all = thumbnailBookRepository.findAllByBookId(bookId)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class LocalArtworkLifecycle(
|
||||||
|
|
||||||
if (library.importLocalArtwork)
|
if (library.importLocalArtwork)
|
||||||
localArtworkProvider.getBookThumbnails(book).forEach {
|
localArtworkProvider.getBookThumbnails(book).forEach {
|
||||||
bookLifecycle.addThumbnailForBook(it)
|
bookLifecycle.addThumbnailForBook(it, if (it.selected) MarkSelectedPreference.IF_NONE_OR_GENERATED else MarkSelectedPreference.NO)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
logger.info { "Library is not set to import local artwork, skipping" }
|
logger.info { "Library is not set to import local artwork, skipping" }
|
||||||
|
|
@ -36,7 +36,7 @@ class LocalArtworkLifecycle(
|
||||||
|
|
||||||
if (library.importLocalArtwork)
|
if (library.importLocalArtwork)
|
||||||
localArtworkProvider.getSeriesThumbnails(series).forEach {
|
localArtworkProvider.getSeriesThumbnails(series).forEach {
|
||||||
seriesLifecycle.addThumbnailForSeries(it, if (it.selected) MarkSelectedPreference.IF_NONE_EXIST else MarkSelectedPreference.NO)
|
seriesLifecycle.addThumbnailForSeries(it, if (it.selected) MarkSelectedPreference.IF_NONE_OR_GENERATED else MarkSelectedPreference.NO)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
logger.info { "Library is not set to import local artwork, skipping" }
|
logger.info { "Library is not set to import local artwork, skipping" }
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,26 @@ import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.DuplicateNameException
|
import org.gotson.komga.domain.model.DuplicateNameException
|
||||||
import org.gotson.komga.domain.model.ReadList
|
import org.gotson.komga.domain.model.ReadList
|
||||||
import org.gotson.komga.domain.model.ReadListRequestResult
|
import org.gotson.komga.domain.model.ReadListRequestResult
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailReadList
|
||||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailReadListRepository
|
||||||
import org.gotson.komga.infrastructure.image.MosaicGenerator
|
import org.gotson.komga.infrastructure.image.MosaicGenerator
|
||||||
import org.gotson.komga.infrastructure.metadata.comicrack.ReadListProvider
|
import org.gotson.komga.infrastructure.metadata.comicrack.ReadListProvider
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ReadListLifecycle(
|
class ReadListLifecycle(
|
||||||
private val readListRepository: ReadListRepository,
|
private val readListRepository: ReadListRepository,
|
||||||
|
private val thumbnailReadListRepository: ThumbnailReadListRepository,
|
||||||
private val bookLifecycle: BookLifecycle,
|
private val bookLifecycle: BookLifecycle,
|
||||||
private val mosaicGenerator: MosaicGenerator,
|
private val mosaicGenerator: MosaicGenerator,
|
||||||
private val readListMatcher: ReadListMatcher,
|
private val readListMatcher: ReadListMatcher,
|
||||||
private val readListProvider: ReadListProvider,
|
private val readListProvider: ReadListProvider,
|
||||||
private val eventPublisher: EventPublisher,
|
private val eventPublisher: EventPublisher,
|
||||||
|
private val transactionTemplate: TransactionTemplate,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
|
|
@ -53,19 +58,57 @@ class ReadListLifecycle(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteReadList(readList: ReadList) {
|
fun deleteReadList(readList: ReadList) {
|
||||||
readListRepository.delete(readList.id)
|
transactionTemplate.executeWithoutResult {
|
||||||
|
thumbnailReadListRepository.deleteByReadListId(readList.id)
|
||||||
|
readListRepository.delete(readList.id)
|
||||||
|
}
|
||||||
|
|
||||||
eventPublisher.publishEvent(DomainEvent.ReadListDeleted(readList))
|
eventPublisher.publishEvent(DomainEvent.ReadListDeleted(readList))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEmptyReadLists() {
|
fun deleteEmptyReadLists() {
|
||||||
logger.info { "Deleting empty read lists" }
|
logger.info { "Deleting empty read lists" }
|
||||||
val toDelete = readListRepository.findAllEmpty()
|
transactionTemplate.executeWithoutResult {
|
||||||
readListRepository.delete(toDelete.map { it.id })
|
val toDelete = readListRepository.findAllEmpty()
|
||||||
toDelete.forEach { eventPublisher.publishEvent(DomainEvent.ReadListDeleted(it)) }
|
readListRepository.delete(toDelete.map { it.id })
|
||||||
|
thumbnailReadListRepository.deleteByReadListIds(toDelete.map { it.id })
|
||||||
|
|
||||||
|
toDelete.forEach { eventPublisher.publishEvent(DomainEvent.ReadListDeleted(it)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addThumbnail(thumbnail: ThumbnailReadList) {
|
||||||
|
when (thumbnail.type) {
|
||||||
|
ThumbnailReadList.Type.USER_UPLOADED -> {
|
||||||
|
thumbnailReadListRepository.insert(thumbnail)
|
||||||
|
if (thumbnail.selected) {
|
||||||
|
thumbnailReadListRepository.markSelected(thumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListAdded(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markSelectedThumbnail(thumbnail: ThumbnailReadList) {
|
||||||
|
thumbnailReadListRepository.markSelected(thumbnail)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListAdded(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteThumbnail(thumbnail: ThumbnailReadList) {
|
||||||
|
thumbnailReadListRepository.delete(thumbnail.id)
|
||||||
|
thumbnailsHouseKeeping(thumbnail.readListId)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListDeleted(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThumbnailBytes(thumbnailId: String): ByteArray? =
|
||||||
|
thumbnailReadListRepository.findByIdOrNull(thumbnailId)?.thumbnail
|
||||||
|
|
||||||
fun getThumbnailBytes(readList: ReadList): ByteArray {
|
fun getThumbnailBytes(readList: ReadList): ByteArray {
|
||||||
|
thumbnailReadListRepository.findSelectedByReadListIdOrNull(readList.id)?.let {
|
||||||
|
return it.thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
val ids = with(mutableListOf<String>()) {
|
val ids = with(mutableListOf<String>()) {
|
||||||
while (size < 4) {
|
while (size < 4) {
|
||||||
this += readList.bookIds.values.take(4)
|
this += readList.bookIds.values.take(4)
|
||||||
|
|
@ -93,4 +136,21 @@ class ReadListLifecycle(
|
||||||
else -> result
|
else -> result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun thumbnailsHouseKeeping(readListId: String) {
|
||||||
|
logger.info { "House keeping thumbnails for read list: $readListId" }
|
||||||
|
val all = thumbnailReadListRepository.findAllByReadListId(readListId)
|
||||||
|
|
||||||
|
val selected = all.filter { it.selected }
|
||||||
|
when {
|
||||||
|
selected.size > 1 -> {
|
||||||
|
logger.info { "More than one thumbnail is selected, removing extra ones" }
|
||||||
|
thumbnailReadListRepository.markSelected(selected[0])
|
||||||
|
}
|
||||||
|
selected.isEmpty() && all.isNotEmpty() -> {
|
||||||
|
logger.info { "Read list has no selected thumbnail, choosing one automatically" }
|
||||||
|
thumbnailReadListRepository.markSelected(all.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,23 @@ import org.gotson.komga.application.events.EventPublisher
|
||||||
import org.gotson.komga.domain.model.DomainEvent
|
import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.DuplicateNameException
|
import org.gotson.komga.domain.model.DuplicateNameException
|
||||||
import org.gotson.komga.domain.model.SeriesCollection
|
import org.gotson.komga.domain.model.SeriesCollection
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
|
||||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailSeriesCollectionRepository
|
||||||
import org.gotson.komga.infrastructure.image.MosaicGenerator
|
import org.gotson.komga.infrastructure.image.MosaicGenerator
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class SeriesCollectionLifecycle(
|
class SeriesCollectionLifecycle(
|
||||||
private val collectionRepository: SeriesCollectionRepository,
|
private val collectionRepository: SeriesCollectionRepository,
|
||||||
|
private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository,
|
||||||
private val seriesLifecycle: SeriesLifecycle,
|
private val seriesLifecycle: SeriesLifecycle,
|
||||||
private val mosaicGenerator: MosaicGenerator,
|
private val mosaicGenerator: MosaicGenerator,
|
||||||
private val eventPublisher: EventPublisher,
|
private val eventPublisher: EventPublisher,
|
||||||
|
private val transactionTemplate: TransactionTemplate,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
|
|
@ -50,18 +55,56 @@ class SeriesCollectionLifecycle(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCollection(collection: SeriesCollection) {
|
fun deleteCollection(collection: SeriesCollection) {
|
||||||
collectionRepository.delete(collection.id)
|
transactionTemplate.executeWithoutResult {
|
||||||
|
thumbnailSeriesCollectionRepository.deleteByCollectionId(collection.id)
|
||||||
|
collectionRepository.delete(collection.id)
|
||||||
|
}
|
||||||
eventPublisher.publishEvent(DomainEvent.CollectionDeleted(collection))
|
eventPublisher.publishEvent(DomainEvent.CollectionDeleted(collection))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEmptyCollections() {
|
fun deleteEmptyCollections() {
|
||||||
logger.info { "Deleting empty collections" }
|
logger.info { "Deleting empty collections" }
|
||||||
val toDelete = collectionRepository.findAllEmpty()
|
transactionTemplate.executeWithoutResult {
|
||||||
collectionRepository.delete(toDelete.map { it.id })
|
val toDelete = collectionRepository.findAllEmpty()
|
||||||
toDelete.forEach { eventPublisher.publishEvent(DomainEvent.CollectionDeleted(it)) }
|
thumbnailSeriesCollectionRepository.deleteByCollectionIds(toDelete.map { it.id })
|
||||||
|
collectionRepository.delete(toDelete.map { it.id })
|
||||||
|
|
||||||
|
toDelete.forEach { eventPublisher.publishEvent(DomainEvent.CollectionDeleted(it)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addThumbnail(thumbnail: ThumbnailSeriesCollection) {
|
||||||
|
when (thumbnail.type) {
|
||||||
|
ThumbnailSeriesCollection.Type.USER_UPLOADED -> {
|
||||||
|
thumbnailSeriesCollectionRepository.insert(thumbnail)
|
||||||
|
if (thumbnail.selected) {
|
||||||
|
thumbnailSeriesCollectionRepository.markSelected(thumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesCollectionAdded(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markSelectedThumbnail(thumbnail: ThumbnailSeriesCollection) {
|
||||||
|
thumbnailSeriesCollectionRepository.markSelected(thumbnail)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesCollectionAdded(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteThumbnail(thumbnail: ThumbnailSeriesCollection) {
|
||||||
|
thumbnailSeriesCollectionRepository.delete(thumbnail.id)
|
||||||
|
thumbnailsHouseKeeping(thumbnail.collectionId)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesCollectionDeleted(thumbnail))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThumbnailBytes(thumbnailId: String): ByteArray? =
|
||||||
|
thumbnailSeriesCollectionRepository.findByIdOrNull(thumbnailId)?.thumbnail
|
||||||
|
|
||||||
fun getThumbnailBytes(collection: SeriesCollection, userId: String): ByteArray {
|
fun getThumbnailBytes(collection: SeriesCollection, userId: String): ByteArray {
|
||||||
|
thumbnailSeriesCollectionRepository.findSelectedByCollectionIdOrNull(collection.id)?.let {
|
||||||
|
return it.thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
val ids = with(mutableListOf<String>()) {
|
val ids = with(mutableListOf<String>()) {
|
||||||
while (size < 4) {
|
while (size < 4) {
|
||||||
this += collection.seriesIds.take(4)
|
this += collection.seriesIds.take(4)
|
||||||
|
|
@ -72,4 +115,21 @@ class SeriesCollectionLifecycle(
|
||||||
val images = ids.mapNotNull { seriesLifecycle.getThumbnailBytes(it, userId) }
|
val images = ids.mapNotNull { seriesLifecycle.getThumbnailBytes(it, userId) }
|
||||||
return mosaicGenerator.createMosaic(images)
|
return mosaicGenerator.createMosaic(images)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun thumbnailsHouseKeeping(collectionId: String) {
|
||||||
|
logger.info { "House keeping thumbnails for collection: $collectionId" }
|
||||||
|
val all = thumbnailSeriesCollectionRepository.findAllByCollectionId(collectionId)
|
||||||
|
|
||||||
|
val selected = all.filter { it.selected }
|
||||||
|
when {
|
||||||
|
selected.size > 1 -> {
|
||||||
|
logger.info { "More than one thumbnail is selected, removing extra ones" }
|
||||||
|
thumbnailSeriesCollectionRepository.markSelected(selected[0])
|
||||||
|
}
|
||||||
|
selected.isEmpty() && all.isNotEmpty() -> {
|
||||||
|
logger.info { "Collection has no selected thumbnail, choosing one automatically" }
|
||||||
|
thumbnailSeriesCollectionRepository.markSelected(all.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ class SeriesLifecycle(
|
||||||
|
|
||||||
if (markSelected == MarkSelectedPreference.YES ||
|
if (markSelected == MarkSelectedPreference.YES ||
|
||||||
(
|
(
|
||||||
markSelected == MarkSelectedPreference.IF_NONE_EXIST &&
|
markSelected == MarkSelectedPreference.IF_NONE_OR_GENERATED &&
|
||||||
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
|
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
@ -276,6 +276,7 @@ class SeriesLifecycle(
|
||||||
fun deleteThumbnailForSeries(thumbnail: ThumbnailSeries) {
|
fun deleteThumbnailForSeries(thumbnail: ThumbnailSeries) {
|
||||||
require(thumbnail.type == ThumbnailSeries.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
|
require(thumbnail.type == ThumbnailSeries.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
|
||||||
thumbnailsSeriesRepository.delete(thumbnail.id)
|
thumbnailsSeriesRepository.delete(thumbnail.id)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesDeleted(thumbnail))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun thumbnailsHouseKeeping(seriesId: String) {
|
private fun thumbnailsHouseKeeping(seriesId: String) {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ class ThumbnailBookDao(
|
||||||
.fetchInto(tb)
|
.fetchInto(tb)
|
||||||
.map { it.toDomain() }
|
.map { it.toDomain() }
|
||||||
|
|
||||||
|
override fun findByIdOrNull(thumbnailId: String): ThumbnailBook? =
|
||||||
|
dsl.selectFrom(tb)
|
||||||
|
.where(tb.ID.eq(thumbnailId))
|
||||||
|
.fetchOneInto(tb)
|
||||||
|
?.toDomain()
|
||||||
|
|
||||||
override fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook? =
|
override fun findSelectedByBookIdOrNull(bookId: String): ThumbnailBook? =
|
||||||
dsl.selectFrom(tb)
|
dsl.selectFrom(tb)
|
||||||
.where(tb.BOOK_ID.eq(bookId))
|
.where(tb.BOOK_ID.eq(bookId))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.gotson.komga.infrastructure.jooq
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailReadList
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailReadListRepository
|
||||||
|
import org.gotson.komga.jooq.Tables
|
||||||
|
import org.gotson.komga.jooq.tables.records.ThumbnailReadlistRecord
|
||||||
|
import org.jooq.DSLContext
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ThumbnailReadListDao(
|
||||||
|
private val dsl: DSLContext
|
||||||
|
) : ThumbnailReadListRepository {
|
||||||
|
private val tr = Tables.THUMBNAIL_READLIST
|
||||||
|
|
||||||
|
override fun findAllByReadListId(readListId: String): Collection<ThumbnailReadList> =
|
||||||
|
dsl.selectFrom(tr)
|
||||||
|
.where(tr.READLIST_ID.eq(readListId))
|
||||||
|
.fetchInto(tr)
|
||||||
|
.map { it.toDomain() }
|
||||||
|
|
||||||
|
override fun findByIdOrNull(thumbnailId: String): ThumbnailReadList? =
|
||||||
|
dsl.selectFrom(tr)
|
||||||
|
.where(tr.ID.eq(thumbnailId))
|
||||||
|
.fetchOneInto(tr)
|
||||||
|
?.toDomain()
|
||||||
|
|
||||||
|
override fun findSelectedByReadListIdOrNull(readListId: String): ThumbnailReadList? =
|
||||||
|
dsl.selectFrom(tr)
|
||||||
|
.where(tr.READLIST_ID.eq(readListId))
|
||||||
|
.and(tr.SELECTED.isTrue)
|
||||||
|
.limit(1)
|
||||||
|
.fetchInto(tr)
|
||||||
|
.map { it.toDomain() }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
override fun insert(thumbnail: ThumbnailReadList) {
|
||||||
|
dsl.insertInto(tr)
|
||||||
|
.set(tr.ID, thumbnail.id)
|
||||||
|
.set(tr.READLIST_ID, thumbnail.readListId)
|
||||||
|
.set(tr.THUMBNAIL, thumbnail.thumbnail)
|
||||||
|
.set(tr.SELECTED, thumbnail.selected)
|
||||||
|
.set(tr.TYPE, thumbnail.type.toString())
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(thumbnail: ThumbnailReadList) {
|
||||||
|
dsl.update(tr)
|
||||||
|
.set(tr.READLIST_ID, thumbnail.readListId)
|
||||||
|
.set(tr.THUMBNAIL, thumbnail.thumbnail)
|
||||||
|
.set(tr.SELECTED, thumbnail.selected)
|
||||||
|
.set(tr.TYPE, thumbnail.type.toString())
|
||||||
|
.where(tr.ID.eq(thumbnail.id))
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
override fun markSelected(thumbnail: ThumbnailReadList) {
|
||||||
|
dsl.update(tr)
|
||||||
|
.set(tr.SELECTED, false)
|
||||||
|
.where(tr.READLIST_ID.eq(thumbnail.readListId))
|
||||||
|
.and(tr.ID.ne(thumbnail.id))
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
dsl.update(tr)
|
||||||
|
.set(tr.SELECTED, true)
|
||||||
|
.where(tr.READLIST_ID.eq(thumbnail.readListId))
|
||||||
|
.and(tr.ID.eq(thumbnail.id))
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(thumbnailReadListId: String) {
|
||||||
|
dsl.deleteFrom(tr).where(tr.ID.eq(thumbnailReadListId)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteByReadListId(readListId: String) {
|
||||||
|
dsl.deleteFrom(tr).where(tr.READLIST_ID.eq(readListId)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteByReadListIds(readListIds: Collection<String>) {
|
||||||
|
dsl.deleteFrom(tr).where(tr.READLIST_ID.`in`(readListIds)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ThumbnailReadlistRecord.toDomain() =
|
||||||
|
ThumbnailReadList(
|
||||||
|
thumbnail = thumbnail,
|
||||||
|
selected = selected,
|
||||||
|
type = ThumbnailReadList.Type.valueOf(type),
|
||||||
|
id = id,
|
||||||
|
readListId = readlistId,
|
||||||
|
createdDate = createdDate,
|
||||||
|
lastModifiedDate = lastModifiedDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.gotson.komga.infrastructure.jooq
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailSeriesCollectionRepository
|
||||||
|
import org.gotson.komga.jooq.Tables
|
||||||
|
import org.gotson.komga.jooq.tables.records.ThumbnailCollectionRecord
|
||||||
|
import org.jooq.DSLContext
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ThumbnailSeriesCollectionDao(
|
||||||
|
private val dsl: DSLContext
|
||||||
|
) : ThumbnailSeriesCollectionRepository {
|
||||||
|
private val tc = Tables.THUMBNAIL_COLLECTION
|
||||||
|
|
||||||
|
override fun findByIdOrNull(thumbnailId: String): ThumbnailSeriesCollection? =
|
||||||
|
dsl.selectFrom(tc)
|
||||||
|
.where(tc.ID.eq(thumbnailId))
|
||||||
|
.fetchOneInto(tc)
|
||||||
|
?.toDomain()
|
||||||
|
|
||||||
|
override fun findSelectedByCollectionIdOrNull(collectionId: String): ThumbnailSeriesCollection? =
|
||||||
|
dsl.selectFrom(tc)
|
||||||
|
.where(tc.COLLECTION_ID.eq(collectionId))
|
||||||
|
.and(tc.SELECTED.isTrue)
|
||||||
|
.limit(1)
|
||||||
|
.fetchInto(tc)
|
||||||
|
.map { it.toDomain() }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
override fun findAllByCollectionId(collectionId: String): Collection<ThumbnailSeriesCollection> =
|
||||||
|
dsl.selectFrom(tc)
|
||||||
|
.where(tc.COLLECTION_ID.eq(collectionId))
|
||||||
|
.fetchInto(tc)
|
||||||
|
.map { it.toDomain() }
|
||||||
|
|
||||||
|
override fun insert(thumbnail: ThumbnailSeriesCollection) {
|
||||||
|
dsl.insertInto(tc)
|
||||||
|
.set(tc.ID, thumbnail.id)
|
||||||
|
.set(tc.COLLECTION_ID, thumbnail.collectionId)
|
||||||
|
.set(tc.THUMBNAIL, thumbnail.thumbnail)
|
||||||
|
.set(tc.SELECTED, thumbnail.selected)
|
||||||
|
.set(tc.TYPE, thumbnail.type.toString())
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(thumbnail: ThumbnailSeriesCollection) {
|
||||||
|
dsl.update(tc)
|
||||||
|
.set(tc.COLLECTION_ID, thumbnail.collectionId)
|
||||||
|
.set(tc.THUMBNAIL, thumbnail.thumbnail)
|
||||||
|
.set(tc.SELECTED, thumbnail.selected)
|
||||||
|
.set(tc.TYPE, thumbnail.type.toString())
|
||||||
|
.where(tc.ID.eq(thumbnail.id))
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
override fun markSelected(thumbnail: ThumbnailSeriesCollection) {
|
||||||
|
dsl.update(tc)
|
||||||
|
.set(tc.SELECTED, false)
|
||||||
|
.where(tc.COLLECTION_ID.eq(thumbnail.collectionId))
|
||||||
|
.and(tc.ID.ne(thumbnail.id))
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
dsl.update(tc)
|
||||||
|
.set(tc.SELECTED, true)
|
||||||
|
.where(tc.COLLECTION_ID.eq(thumbnail.collectionId))
|
||||||
|
.and(tc.ID.eq(thumbnail.id))
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(thumbnailCollectionId: String) {
|
||||||
|
dsl.deleteFrom(tc).where(tc.ID.eq(thumbnailCollectionId)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteByCollectionId(collectionId: String) {
|
||||||
|
dsl.deleteFrom(tc).where(tc.COLLECTION_ID.eq(collectionId)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteByCollectionIds(collectionIds: Collection<String>) {
|
||||||
|
dsl.deleteFrom(tc).where(tc.COLLECTION_ID.`in`(collectionIds)).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ThumbnailCollectionRecord.toDomain() =
|
||||||
|
ThumbnailSeriesCollection(
|
||||||
|
thumbnail = thumbnail,
|
||||||
|
selected = selected,
|
||||||
|
type = ThumbnailSeriesCollection.Type.valueOf(type),
|
||||||
|
id = id,
|
||||||
|
collectionId = collectionId,
|
||||||
|
createdDate = createdDate,
|
||||||
|
lastModifiedDate = lastModifiedDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,16 +14,19 @@ import org.gotson.komga.application.tasks.TaskReceiver
|
||||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||||
import org.gotson.komga.domain.model.DomainEvent
|
import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.ImageConversionException
|
import org.gotson.komga.domain.model.ImageConversionException
|
||||||
|
import org.gotson.komga.domain.model.MarkSelectedPreference
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||||
import org.gotson.komga.domain.model.ReadStatus
|
import org.gotson.komga.domain.model.ReadStatus
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailBook
|
||||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.MediaRepository
|
import org.gotson.komga.domain.persistence.MediaRepository
|
||||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailBookRepository
|
||||||
import org.gotson.komga.domain.service.BookLifecycle
|
import org.gotson.komga.domain.service.BookLifecycle
|
||||||
import org.gotson.komga.infrastructure.image.ImageType
|
import org.gotson.komga.infrastructure.image.ImageType
|
||||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||||
|
|
@ -40,6 +43,7 @@ import org.gotson.komga.interfaces.api.rest.dto.BookMetadataUpdateDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.PageDto
|
import org.gotson.komga.interfaces.api.rest.dto.PageDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.ReadListDto
|
import org.gotson.komga.interfaces.api.rest.dto.ReadListDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.ReadProgressUpdateDto
|
import org.gotson.komga.interfaces.api.rest.dto.ReadProgressUpdateDto
|
||||||
|
import org.gotson.komga.interfaces.api.rest.dto.ThumbnailBookDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.patch
|
import org.gotson.komga.interfaces.api.rest.dto.patch
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.restrictUrl
|
import org.gotson.komga.interfaces.api.rest.dto.restrictUrl
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||||
|
|
@ -61,6 +65,7 @@ import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PatchMapping
|
import org.springframework.web.bind.annotation.PatchMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
|
@ -68,6 +73,7 @@ import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.context.request.ServletWebRequest
|
import org.springframework.web.context.request.ServletWebRequest
|
||||||
import org.springframework.web.context.request.WebRequest
|
import org.springframework.web.context.request.WebRequest
|
||||||
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
@ -92,6 +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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PageableAsQueryParam
|
@PageableAsQueryParam
|
||||||
|
|
@ -246,6 +253,99 @@ class BookController(
|
||||||
return bookLifecycle.getThumbnailBytes(bookId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
return bookLifecycle.getThumbnailBytes(bookId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
|
||||||
|
@GetMapping(value = ["api/v1/books/{bookId}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||||
|
fun getBookThumbnailById(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "bookId") bookId: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String
|
||||||
|
): ByteArray {
|
||||||
|
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||||
|
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
return bookLifecycle.getThumbnailBytesByThumbnailId(thumbnailId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = ["api/v1/books/{bookId}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
fun getBookThumbnails(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "bookId") bookId: String,
|
||||||
|
): Collection<ThumbnailBookDto> {
|
||||||
|
bookRepository.getLibraryIdOrNull(bookId)?.let {
|
||||||
|
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
return thumbnailBookRepository.findAllByBookId(bookId)
|
||||||
|
.map { it.toDto() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = ["api/v1/books/{bookId}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun addUserUploadedBookThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "bookId") bookId: String,
|
||||||
|
@RequestParam("file") file: MultipartFile,
|
||||||
|
@RequestParam("selected") selected: Boolean = true,
|
||||||
|
) {
|
||||||
|
val book = bookRepository.findByIdOrNull(bookId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
|
|
||||||
|
if (!contentDetector.isImage(file.inputStream.buffered().use { contentDetector.detectMediaType(it) }))
|
||||||
|
throw ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||||
|
|
||||||
|
bookLifecycle.addThumbnailForBook(
|
||||||
|
ThumbnailBook(
|
||||||
|
bookId = book.id,
|
||||||
|
thumbnail = file.bytes,
|
||||||
|
type = ThumbnailBook.Type.USER_UPLOADED,
|
||||||
|
selected = selected
|
||||||
|
),
|
||||||
|
if (selected) MarkSelectedPreference.YES else MarkSelectedPreference.NO
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("api/v1/books/{bookId}/thumbnails/{thumbnailId}/selected")
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun markSelectedBookThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "bookId") bookId: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||||
|
) {
|
||||||
|
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||||
|
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
thumbnailBookRepository.markSelected(it)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(it))
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("api/v1/books/{bookId}/thumbnails/{thumbnailId}")
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun deleteUserUploadedBookThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "bookId") bookId: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||||
|
) {
|
||||||
|
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||||
|
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
try {
|
||||||
|
bookLifecycle.deleteThumbnailForBook(it)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
|
||||||
|
}
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(description = "Download the book file.")
|
@Operation(description = "Download the book file.")
|
||||||
@GetMapping(
|
@GetMapping(
|
||||||
value = [
|
value = [
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||||
import org.gotson.komga.domain.model.ReadList
|
import org.gotson.komga.domain.model.ReadList
|
||||||
import org.gotson.komga.domain.model.ReadStatus
|
import org.gotson.komga.domain.model.ReadStatus
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailReadList
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailReadListRepository
|
||||||
import org.gotson.komga.domain.service.BookLifecycle
|
import org.gotson.komga.domain.service.BookLifecycle
|
||||||
import org.gotson.komga.domain.service.ReadListLifecycle
|
import org.gotson.komga.domain.service.ReadListLifecycle
|
||||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||||
import org.gotson.komga.infrastructure.language.toIndexedMap
|
import org.gotson.komga.infrastructure.language.toIndexedMap
|
||||||
|
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
|
||||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||||
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
|
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
|
||||||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||||
|
|
@ -35,6 +38,7 @@ import org.gotson.komga.interfaces.api.rest.dto.ReadListRequestResultDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.ReadListUpdateDto
|
import org.gotson.komga.interfaces.api.rest.dto.ReadListUpdateDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.TachiyomiReadProgressDto
|
import org.gotson.komga.interfaces.api.rest.dto.TachiyomiReadProgressDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.TachiyomiReadProgressUpdateDto
|
import org.gotson.komga.interfaces.api.rest.dto.TachiyomiReadProgressUpdateDto
|
||||||
|
import org.gotson.komga.interfaces.api.rest.dto.ThumbnailReadListDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.restrictUrl
|
import org.gotson.komga.interfaces.api.rest.dto.restrictUrl
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||||
import org.springframework.core.io.FileSystemResource
|
import org.springframework.core.io.FileSystemResource
|
||||||
|
|
@ -79,6 +83,8 @@ class ReadListController(
|
||||||
private val bookDtoRepository: BookDtoRepository,
|
private val bookDtoRepository: BookDtoRepository,
|
||||||
private val bookRepository: BookRepository,
|
private val bookRepository: BookRepository,
|
||||||
private val readProgressDtoRepository: ReadProgressDtoRepository,
|
private val readProgressDtoRepository: ReadProgressDtoRepository,
|
||||||
|
private val thumbnailReadListRepository: ThumbnailReadListRepository,
|
||||||
|
private val contentDetector: ContentDetector,
|
||||||
private val bookLifecycle: BookLifecycle,
|
private val bookLifecycle: BookLifecycle,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -152,6 +158,84 @@ class ReadListController(
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
|
||||||
|
@GetMapping(value = ["{id}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||||
|
fun getReadListThumbnailById(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String
|
||||||
|
): ByteArray {
|
||||||
|
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||||
|
return readListLifecycle.getThumbnailBytes(thumbnailId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = ["{id}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
fun getReadListThumbnails(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
): Collection<ThumbnailReadListDto> {
|
||||||
|
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||||
|
return thumbnailReadListRepository.findAllByReadListId(id).map { it.toDto() }
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = ["{id}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun addUserUploadedReadListThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@RequestParam("file") file: MultipartFile,
|
||||||
|
@RequestParam("selected") selected: Boolean = true,
|
||||||
|
) {
|
||||||
|
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||||
|
|
||||||
|
if (!contentDetector.isImage(file.inputStream.buffered().use { contentDetector.detectMediaType(it) }))
|
||||||
|
throw ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||||
|
|
||||||
|
readListLifecycle.addThumbnail(
|
||||||
|
ThumbnailReadList(
|
||||||
|
readListId = readList.id,
|
||||||
|
thumbnail = file.bytes,
|
||||||
|
type = ThumbnailReadList.Type.USER_UPLOADED,
|
||||||
|
selected = selected
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("{id}/thumbnails/{thumbnailId}/selected")
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun markSelectedReadListThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||||
|
) {
|
||||||
|
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||||
|
thumbnailReadListRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
readListLifecycle.markSelectedThumbnail(it)
|
||||||
|
}
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{id}/thumbnails/{thumbnailId}")
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun deleteUserUploadedReadListThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||||
|
) {
|
||||||
|
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||||
|
thumbnailReadListRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
readListLifecycle.deleteThumbnail(it)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
fun addOne(
|
fun addOne(
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,12 @@ import org.gotson.komga.domain.model.ReadStatus
|
||||||
import org.gotson.komga.domain.model.SeriesCollection
|
import org.gotson.komga.domain.model.SeriesCollection
|
||||||
import org.gotson.komga.domain.model.SeriesMetadata
|
import org.gotson.komga.domain.model.SeriesMetadata
|
||||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
|
||||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||||
|
import org.gotson.komga.domain.persistence.ThumbnailSeriesCollectionRepository
|
||||||
import org.gotson.komga.domain.service.SeriesCollectionLifecycle
|
import org.gotson.komga.domain.service.SeriesCollectionLifecycle
|
||||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
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.security.KomgaPrincipal
|
||||||
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
|
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
|
||||||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||||
|
|
@ -24,6 +27,7 @@ import org.gotson.komga.interfaces.api.rest.dto.CollectionCreationDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.CollectionDto
|
import org.gotson.komga.interfaces.api.rest.dto.CollectionDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.CollectionUpdateDto
|
import org.gotson.komga.interfaces.api.rest.dto.CollectionUpdateDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.SeriesDto
|
import org.gotson.komga.interfaces.api.rest.dto.SeriesDto
|
||||||
|
import org.gotson.komga.interfaces.api.rest.dto.ThumbnailSeriesCollectionDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.restrictUrl
|
import org.gotson.komga.interfaces.api.rest.dto.restrictUrl
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
|
|
@ -41,11 +45,13 @@ import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PatchMapping
|
import org.springframework.web.bind.annotation.PatchMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.validation.Valid
|
import javax.validation.Valid
|
||||||
|
|
@ -57,7 +63,9 @@ private val logger = KotlinLogging.logger {}
|
||||||
class SeriesCollectionController(
|
class SeriesCollectionController(
|
||||||
private val collectionRepository: SeriesCollectionRepository,
|
private val collectionRepository: SeriesCollectionRepository,
|
||||||
private val collectionLifecycle: SeriesCollectionLifecycle,
|
private val collectionLifecycle: SeriesCollectionLifecycle,
|
||||||
private val seriesDtoRepository: SeriesDtoRepository
|
private val seriesDtoRepository: SeriesDtoRepository,
|
||||||
|
private val contentDetector: ContentDetector,
|
||||||
|
private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PageableWithoutSortAsQueryParam
|
@PageableWithoutSortAsQueryParam
|
||||||
|
|
@ -112,6 +120,84 @@ class SeriesCollectionController(
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
|
||||||
|
@GetMapping(value = ["{id}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||||
|
fun getCollectionThumbnailById(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String
|
||||||
|
): ByteArray {
|
||||||
|
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||||
|
return collectionLifecycle.getThumbnailBytes(thumbnailId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = ["{id}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
fun getCollectionThumbnails(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
): Collection<ThumbnailSeriesCollectionDto> {
|
||||||
|
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||||
|
return thumbnailSeriesCollectionRepository.findAllByCollectionId(id).map { it.toDto() }
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = ["{id}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun addUserUploadedCollectionThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@RequestParam("file") file: MultipartFile,
|
||||||
|
@RequestParam("selected") selected: Boolean = true,
|
||||||
|
) {
|
||||||
|
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { collection ->
|
||||||
|
|
||||||
|
if (!contentDetector.isImage(file.inputStream.buffered().use { contentDetector.detectMediaType(it) }))
|
||||||
|
throw ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||||
|
|
||||||
|
collectionLifecycle.addThumbnail(
|
||||||
|
ThumbnailSeriesCollection(
|
||||||
|
collectionId = collection.id,
|
||||||
|
thumbnail = file.bytes,
|
||||||
|
type = ThumbnailSeriesCollection.Type.USER_UPLOADED,
|
||||||
|
selected = selected
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("{id}/thumbnails/{thumbnailId}/selected")
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun markSelectedCollectionThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||||
|
) {
|
||||||
|
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||||
|
thumbnailSeriesCollectionRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
collectionLifecycle.markSelectedThumbnail(it)
|
||||||
|
}
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{id}/thumbnails/{thumbnailId}")
|
||||||
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun deleteUserUploadedCollectionThumbnail(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@PathVariable(name = "id") id: String,
|
||||||
|
@PathVariable(name = "thumbnailId") thumbnailId: String,
|
||||||
|
) {
|
||||||
|
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||||
|
thumbnailSeriesCollectionRepository.findByIdOrNull(thumbnailId)?.let {
|
||||||
|
collectionLifecycle.deleteThumbnail(it)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||||
fun addOne(
|
fun addOne(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailBook
|
||||||
|
|
||||||
|
data class ThumbnailBookDto(
|
||||||
|
val id: String,
|
||||||
|
val bookId: String,
|
||||||
|
val type: String,
|
||||||
|
val selected: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ThumbnailBook.toDto() =
|
||||||
|
ThumbnailBookDto(
|
||||||
|
id = id,
|
||||||
|
bookId = bookId,
|
||||||
|
type = type.toString(),
|
||||||
|
selected = selected
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailReadList
|
||||||
|
|
||||||
|
data class ThumbnailReadListDto(
|
||||||
|
val id: String,
|
||||||
|
val readListId: String,
|
||||||
|
val type: String,
|
||||||
|
val selected: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ThumbnailReadList.toDto() =
|
||||||
|
ThumbnailReadListDto(
|
||||||
|
id = id,
|
||||||
|
readListId = readListId,
|
||||||
|
type = type.toString(),
|
||||||
|
selected = selected
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
|
||||||
|
|
||||||
|
data class ThumbnailSeriesCollectionDto(
|
||||||
|
val id: String,
|
||||||
|
val collectionId: String,
|
||||||
|
val type: String,
|
||||||
|
val selected: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ThumbnailSeriesCollection.toDto() =
|
||||||
|
ThumbnailSeriesCollectionDto(
|
||||||
|
id = id,
|
||||||
|
collectionId = collectionId,
|
||||||
|
type = type.toString(),
|
||||||
|
selected = selected
|
||||||
|
)
|
||||||
|
|
@ -21,6 +21,8 @@ import org.gotson.komga.interfaces.sse.dto.ReadProgressSseDto
|
||||||
import org.gotson.komga.interfaces.sse.dto.SeriesSseDto
|
import org.gotson.komga.interfaces.sse.dto.SeriesSseDto
|
||||||
import org.gotson.komga.interfaces.sse.dto.TaskQueueSseDto
|
import org.gotson.komga.interfaces.sse.dto.TaskQueueSseDto
|
||||||
import org.gotson.komga.interfaces.sse.dto.ThumbnailBookSseDto
|
import org.gotson.komga.interfaces.sse.dto.ThumbnailBookSseDto
|
||||||
|
import org.gotson.komga.interfaces.sse.dto.ThumbnailReadListSseDto
|
||||||
|
import org.gotson.komga.interfaces.sse.dto.ThumbnailSeriesCollectionSseDto
|
||||||
import org.gotson.komga.interfaces.sse.dto.ThumbnailSeriesSseDto
|
import org.gotson.komga.interfaces.sse.dto.ThumbnailSeriesSseDto
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.jms.annotation.JmsListener
|
import org.springframework.jms.annotation.JmsListener
|
||||||
|
|
@ -100,7 +102,13 @@ class SseController(
|
||||||
is DomainEvent.ReadProgressSeriesDeleted -> emitSse("ReadProgressSeriesDeleted", ReadProgressSeriesSseDto(event.seriesId, event.userId), userIdOnly = event.userId)
|
is DomainEvent.ReadProgressSeriesDeleted -> emitSse("ReadProgressSeriesDeleted", ReadProgressSeriesSseDto(event.seriesId, event.userId), userIdOnly = event.userId)
|
||||||
|
|
||||||
is DomainEvent.ThumbnailBookAdded -> emitSse("ThumbnailBookAdded", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty()))
|
is DomainEvent.ThumbnailBookAdded -> emitSse("ThumbnailBookAdded", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty()))
|
||||||
|
is DomainEvent.ThumbnailBookDeleted -> emitSse("ThumbnailBookDeleted", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty()))
|
||||||
is DomainEvent.ThumbnailSeriesAdded -> emitSse("ThumbnailSeriesAdded", ThumbnailSeriesSseDto(event.thumbnail.seriesId))
|
is DomainEvent.ThumbnailSeriesAdded -> emitSse("ThumbnailSeriesAdded", ThumbnailSeriesSseDto(event.thumbnail.seriesId))
|
||||||
|
is DomainEvent.ThumbnailSeriesDeleted -> emitSse("ThumbnailSeriesDeleted", ThumbnailSeriesSseDto(event.thumbnail.seriesId))
|
||||||
|
is DomainEvent.ThumbnailSeriesCollectionAdded -> emitSse("ThumbnailSeriesCollectionAdded", ThumbnailSeriesCollectionSseDto(event.thumbnail.collectionId))
|
||||||
|
is DomainEvent.ThumbnailSeriesCollectionDeleted -> emitSse("ThumbnailSeriesCollectionDeleted", ThumbnailSeriesCollectionSseDto(event.thumbnail.collectionId))
|
||||||
|
is DomainEvent.ThumbnailReadListAdded -> emitSse("ThumbnailReadListAdded", ThumbnailReadListSseDto(event.thumbnail.readListId))
|
||||||
|
is DomainEvent.ThumbnailReadListDeleted -> emitSse("ThumbnailReadListDeleted", ThumbnailReadListSseDto(event.thumbnail.readListId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class ThumbnailReadListSseDto(
|
||||||
|
val readListId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class ThumbnailSeriesCollectionSseDto(
|
||||||
|
val collectionId: String,
|
||||||
|
)
|
||||||
|
|
@ -13,6 +13,7 @@ import org.gotson.komga.domain.model.Book
|
||||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
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.Media
|
||||||
import org.gotson.komga.domain.model.ReadList
|
import org.gotson.komga.domain.model.ReadList
|
||||||
import org.gotson.komga.domain.model.ScanResult
|
import org.gotson.komga.domain.model.ScanResult
|
||||||
|
|
@ -577,8 +578,8 @@ class LibraryContentLifecycleTest(
|
||||||
bookRepository.findByIdOrNull(book.id)?.let {
|
bookRepository.findByIdOrNull(book.id)?.let {
|
||||||
bookRepository.update(it.copy(fileHash = "sameHash"))
|
bookRepository.update(it.copy(fileHash = "sameHash"))
|
||||||
mediaRepository.update(mediaRepository.findById(it.id).copy(status = Media.Status.READY))
|
mediaRepository.update(mediaRepository.findById(it.id).copy(status = Media.Status.READY))
|
||||||
bookLifecycle.addThumbnailForBook(ThumbnailBook(thumbnail = ByteArray(0), type = ThumbnailBook.Type.GENERATED, bookId = book.id))
|
bookLifecycle.addThumbnailForBook(ThumbnailBook(thumbnail = ByteArray(0), type = ThumbnailBook.Type.GENERATED, bookId = book.id), MarkSelectedPreference.NO)
|
||||||
bookLifecycle.addThumbnailForBook(ThumbnailBook(url = URL("file:/sidecar"), type = ThumbnailBook.Type.SIDECAR, bookId = book.id))
|
bookLifecycle.addThumbnailForBook(ThumbnailBook(url = URL("file:/sidecar"), type = ThumbnailBook.Type.SIDECAR, bookId = book.id), MarkSelectedPreference.NO)
|
||||||
}
|
}
|
||||||
|
|
||||||
every { mockHasher.computeHash(any()) } returns "sameHash"
|
every { mockHasher.computeHash(any()) } returns "sameHash"
|
||||||
|
|
@ -623,8 +624,8 @@ class LibraryContentLifecycleTest(
|
||||||
bookRepository.findByIdOrNull(book.id)?.let {
|
bookRepository.findByIdOrNull(book.id)?.let {
|
||||||
bookRepository.update(it.copy(fileHash = "sameHash"))
|
bookRepository.update(it.copy(fileHash = "sameHash"))
|
||||||
mediaRepository.update(mediaRepository.findById(it.id).copy(status = Media.Status.READY))
|
mediaRepository.update(mediaRepository.findById(it.id).copy(status = Media.Status.READY))
|
||||||
bookLifecycle.addThumbnailForBook(ThumbnailBook(thumbnail = ByteArray(0), type = ThumbnailBook.Type.GENERATED, bookId = book.id))
|
bookLifecycle.addThumbnailForBook(ThumbnailBook(thumbnail = ByteArray(0), type = ThumbnailBook.Type.GENERATED, bookId = book.id), MarkSelectedPreference.NO)
|
||||||
bookLifecycle.addThumbnailForBook(ThumbnailBook(url = URL("file:/sidecar"), type = ThumbnailBook.Type.SIDECAR, bookId = book.id))
|
bookLifecycle.addThumbnailForBook(ThumbnailBook(url = URL("file:/sidecar"), type = ThumbnailBook.Type.SIDECAR, bookId = book.id), MarkSelectedPreference.NO)
|
||||||
}
|
}
|
||||||
|
|
||||||
every { mockHasher.computeHash(any()) } returns "sameHash"
|
every { mockHasher.computeHash(any()) } returns "sameHash"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import org.assertj.core.groups.Tuple.tuple
|
||||||
import org.gotson.komga.domain.model.Author
|
import org.gotson.komga.domain.model.Author
|
||||||
import org.gotson.komga.domain.model.BookPage
|
import org.gotson.komga.domain.model.BookPage
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
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.Media
|
||||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||||
import org.gotson.komga.domain.model.ThumbnailBook
|
import org.gotson.komga.domain.model.ThumbnailBook
|
||||||
|
|
@ -474,7 +475,8 @@ class BookControllerTest(
|
||||||
thumbnail = Random.nextBytes(100),
|
thumbnail = Random.nextBytes(100),
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
type = ThumbnailBook.Type.GENERATED
|
type = ThumbnailBook.Type.GENERATED
|
||||||
)
|
),
|
||||||
|
MarkSelectedPreference.YES
|
||||||
)
|
)
|
||||||
|
|
||||||
val url = "/api/v1/books/${book.id}/thumbnail"
|
val url = "/api/v1/books/${book.id}/thumbnail"
|
||||||
|
|
@ -533,7 +535,8 @@ class BookControllerTest(
|
||||||
thumbnail = Random.nextBytes(1),
|
thumbnail = Random.nextBytes(1),
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
type = ThumbnailBook.Type.GENERATED
|
type = ThumbnailBook.Type.GENERATED
|
||||||
)
|
),
|
||||||
|
MarkSelectedPreference.YES
|
||||||
)
|
)
|
||||||
|
|
||||||
val url = "/api/v1/books/${book.id}/thumbnail"
|
val url = "/api/v1/books/${book.id}/thumbnail"
|
||||||
|
|
@ -546,7 +549,8 @@ class BookControllerTest(
|
||||||
thumbnail = Random.nextBytes(1),
|
thumbnail = Random.nextBytes(1),
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
type = ThumbnailBook.Type.GENERATED
|
type = ThumbnailBook.Type.GENERATED
|
||||||
)
|
),
|
||||||
|
MarkSelectedPreference.YES
|
||||||
)
|
)
|
||||||
|
|
||||||
mockMvc.get(url) {
|
mockMvc.get(url) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package org.gotson.komga.interfaces.api.rest
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.gotson.komga.domain.model.BookPage
|
import org.gotson.komga.domain.model.BookPage
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
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.Media
|
||||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||||
import org.gotson.komga.domain.model.SeriesMetadata
|
import org.gotson.komga.domain.model.SeriesMetadata
|
||||||
|
|
@ -580,7 +581,8 @@ class SeriesControllerTest(
|
||||||
thumbnail = Random.nextBytes(1),
|
thumbnail = Random.nextBytes(1),
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
type = ThumbnailBook.Type.GENERATED
|
type = ThumbnailBook.Type.GENERATED
|
||||||
)
|
),
|
||||||
|
MarkSelectedPreference.YES
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -614,7 +616,8 @@ class SeriesControllerTest(
|
||||||
thumbnail = Random.nextBytes(1),
|
thumbnail = Random.nextBytes(1),
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
type = ThumbnailBook.Type.GENERATED
|
type = ThumbnailBook.Type.GENERATED
|
||||||
)
|
),
|
||||||
|
MarkSelectedPreference.YES
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue