mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 07:23:34 +01: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 ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent()
|
||||
data class ThumbnailBookDeleted(val thumbnail: ThumbnailBook) : 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
|
||||
|
||||
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,
|
||||
) : Auditable(), Serializable {
|
||||
enum class Type {
|
||||
GENERATED, SIDECAR
|
||||
GENERATED, SIDECAR, USER_UPLOADED
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
interface ThumbnailBookRepository {
|
||||
fun findByIdOrNull(thumbnailId: String): ThumbnailBook?
|
||||
|
||||
fun findSelectedByBookIdOrNull(bookId: String): 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.ImageConversionException
|
||||
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.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
|
|
@ -82,18 +83,18 @@ class BookLifecycle(
|
|||
fun generateThumbnailAndPersist(book: Book) {
|
||||
logger.info { "Generate thumbnail and persist for book: $book" }
|
||||
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) {
|
||||
logger.error(ex) { "Error while creating thumbnail" }
|
||||
}
|
||||
}
|
||||
|
||||
fun addThumbnailForBook(thumbnail: ThumbnailBook) {
|
||||
fun addThumbnailForBook(thumbnail: ThumbnailBook, markSelected: MarkSelectedPreference) {
|
||||
when (thumbnail.type) {
|
||||
ThumbnailBook.Type.GENERATED -> {
|
||||
// only one generated thumbnail is allowed
|
||||
thumbnailBookRepository.deleteByBookIdAndType(thumbnail.bookId, ThumbnailBook.Type.GENERATED)
|
||||
thumbnailBookRepository.insert(thumbnail)
|
||||
thumbnailBookRepository.insert(thumbnail.copy(selected = false))
|
||||
}
|
||||
ThumbnailBook.Type.SIDECAR -> {
|
||||
// delete existing thumbnail with the same url
|
||||
|
|
@ -102,16 +103,37 @@ class BookLifecycle(
|
|||
.forEach {
|
||||
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))
|
||||
}
|
||||
|
||||
if (thumbnail.selected)
|
||||
thumbnailBookRepository.markSelected(thumbnail)
|
||||
else
|
||||
thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
fun deleteThumbnailForBook(thumbnail: ThumbnailBook) {
|
||||
require(thumbnail.type == ThumbnailBook.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
|
||||
thumbnailBookRepository.delete(thumbnail.id)
|
||||
thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookDeleted(thumbnail))
|
||||
}
|
||||
|
||||
fun getThumbnail(bookId: String): ThumbnailBook? {
|
||||
|
|
@ -136,6 +158,18 @@ class BookLifecycle(
|
|||
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) {
|
||||
logger.info { "House keeping thumbnails for book: $bookId" }
|
||||
val all = thumbnailBookRepository.findAllByBookId(bookId)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class LocalArtworkLifecycle(
|
|||
|
||||
if (library.importLocalArtwork)
|
||||
localArtworkProvider.getBookThumbnails(book).forEach {
|
||||
bookLifecycle.addThumbnailForBook(it)
|
||||
bookLifecycle.addThumbnailForBook(it, if (it.selected) MarkSelectedPreference.IF_NONE_OR_GENERATED else MarkSelectedPreference.NO)
|
||||
}
|
||||
else
|
||||
logger.info { "Library is not set to import local artwork, skipping" }
|
||||
|
|
@ -36,7 +36,7 @@ class LocalArtworkLifecycle(
|
|||
|
||||
if (library.importLocalArtwork)
|
||||
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
|
||||
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.ReadList
|
||||
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.ThumbnailReadListRepository
|
||||
import org.gotson.komga.infrastructure.image.MosaicGenerator
|
||||
import org.gotson.komga.infrastructure.metadata.comicrack.ReadListProvider
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class ReadListLifecycle(
|
||||
private val readListRepository: ReadListRepository,
|
||||
private val thumbnailReadListRepository: ThumbnailReadListRepository,
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
private val mosaicGenerator: MosaicGenerator,
|
||||
private val readListMatcher: ReadListMatcher,
|
||||
private val readListProvider: ReadListProvider,
|
||||
private val eventPublisher: EventPublisher,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
) {
|
||||
|
||||
@Throws(
|
||||
|
|
@ -53,19 +58,57 @@ class ReadListLifecycle(
|
|||
}
|
||||
|
||||
fun deleteReadList(readList: ReadList) {
|
||||
readListRepository.delete(readList.id)
|
||||
transactionTemplate.executeWithoutResult {
|
||||
thumbnailReadListRepository.deleteByReadListId(readList.id)
|
||||
readListRepository.delete(readList.id)
|
||||
}
|
||||
|
||||
eventPublisher.publishEvent(DomainEvent.ReadListDeleted(readList))
|
||||
}
|
||||
|
||||
fun deleteEmptyReadLists() {
|
||||
logger.info { "Deleting empty read lists" }
|
||||
val toDelete = readListRepository.findAllEmpty()
|
||||
readListRepository.delete(toDelete.map { it.id })
|
||||
toDelete.forEach { eventPublisher.publishEvent(DomainEvent.ReadListDeleted(it)) }
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val toDelete = readListRepository.findAllEmpty()
|
||||
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 {
|
||||
thumbnailReadListRepository.findSelectedByReadListIdOrNull(readList.id)?.let {
|
||||
return it.thumbnail
|
||||
}
|
||||
|
||||
val ids = with(mutableListOf<String>()) {
|
||||
while (size < 4) {
|
||||
this += readList.bookIds.values.take(4)
|
||||
|
|
@ -93,4 +136,21 @@ class ReadListLifecycle(
|
|||
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.DuplicateNameException
|
||||
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.ThumbnailSeriesCollectionRepository
|
||||
import org.gotson.komga.infrastructure.image.MosaicGenerator
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class SeriesCollectionLifecycle(
|
||||
private val collectionRepository: SeriesCollectionRepository,
|
||||
private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository,
|
||||
private val seriesLifecycle: SeriesLifecycle,
|
||||
private val mosaicGenerator: MosaicGenerator,
|
||||
private val eventPublisher: EventPublisher,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
) {
|
||||
|
||||
@Throws(
|
||||
|
|
@ -50,18 +55,56 @@ class SeriesCollectionLifecycle(
|
|||
}
|
||||
|
||||
fun deleteCollection(collection: SeriesCollection) {
|
||||
collectionRepository.delete(collection.id)
|
||||
transactionTemplate.executeWithoutResult {
|
||||
thumbnailSeriesCollectionRepository.deleteByCollectionId(collection.id)
|
||||
collectionRepository.delete(collection.id)
|
||||
}
|
||||
eventPublisher.publishEvent(DomainEvent.CollectionDeleted(collection))
|
||||
}
|
||||
|
||||
fun deleteEmptyCollections() {
|
||||
logger.info { "Deleting empty collections" }
|
||||
val toDelete = collectionRepository.findAllEmpty()
|
||||
collectionRepository.delete(toDelete.map { it.id })
|
||||
toDelete.forEach { eventPublisher.publishEvent(DomainEvent.CollectionDeleted(it)) }
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val toDelete = collectionRepository.findAllEmpty()
|
||||
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 {
|
||||
thumbnailSeriesCollectionRepository.findSelectedByCollectionIdOrNull(collection.id)?.let {
|
||||
return it.thumbnail
|
||||
}
|
||||
|
||||
val ids = with(mutableListOf<String>()) {
|
||||
while (size < 4) {
|
||||
this += collection.seriesIds.take(4)
|
||||
|
|
@ -72,4 +115,21 @@ class SeriesCollectionLifecycle(
|
|||
val images = ids.mapNotNull { seriesLifecycle.getThumbnailBytes(it, userId) }
|
||||
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 ||
|
||||
(
|
||||
markSelected == MarkSelectedPreference.IF_NONE_EXIST &&
|
||||
markSelected == MarkSelectedPreference.IF_NONE_OR_GENERATED &&
|
||||
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
|
||||
)
|
||||
) {
|
||||
|
|
@ -276,6 +276,7 @@ class SeriesLifecycle(
|
|||
fun deleteThumbnailForSeries(thumbnail: ThumbnailSeries) {
|
||||
require(thumbnail.type == ThumbnailSeries.Type.USER_UPLOADED) { "Only uploaded thumbnails can be deleted" }
|
||||
thumbnailsSeriesRepository.delete(thumbnail.id)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesDeleted(thumbnail))
|
||||
}
|
||||
|
||||
private fun thumbnailsHouseKeeping(seriesId: String) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ class ThumbnailBookDao(
|
|||
.fetchInto(tb)
|
||||
.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? =
|
||||
dsl.selectFrom(tb)
|
||||
.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.DomainEvent
|
||||
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.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||
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.BookRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
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.infrastructure.image.ImageType
|
||||
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.ReadListDto
|
||||
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.restrictUrl
|
||||
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.PathVariable
|
||||
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.RequestMapping
|
||||
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.context.request.ServletWebRequest
|
||||
import org.springframework.web.context.request.WebRequest
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
|
||||
import java.io.FileNotFoundException
|
||||
|
|
@ -92,6 +98,7 @@ class BookController(
|
|||
private val readListRepository: ReadListRepository,
|
||||
private val contentDetector: ContentDetector,
|
||||
private val eventPublisher: EventPublisher,
|
||||
private val thumbnailBookRepository: ThumbnailBookRepository
|
||||
) {
|
||||
|
||||
@PageableAsQueryParam
|
||||
|
|
@ -246,6 +253,99 @@ class BookController(
|
|||
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.")
|
||||
@GetMapping(
|
||||
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.ReadList
|
||||
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.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.ThumbnailReadListRepository
|
||||
import org.gotson.komga.domain.service.BookLifecycle
|
||||
import org.gotson.komga.domain.service.ReadListLifecycle
|
||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||
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.swagger.AuthorsAsQueryParam
|
||||
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.TachiyomiReadProgressDto
|
||||
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.toDto
|
||||
import org.springframework.core.io.FileSystemResource
|
||||
|
|
@ -79,6 +83,8 @@ class ReadListController(
|
|||
private val bookDtoRepository: BookDtoRepository,
|
||||
private val bookRepository: BookRepository,
|
||||
private val readProgressDtoRepository: ReadProgressDtoRepository,
|
||||
private val thumbnailReadListRepository: ThumbnailReadListRepository,
|
||||
private val contentDetector: ContentDetector,
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
) {
|
||||
|
||||
|
|
@ -152,6 +158,84 @@ class ReadListController(
|
|||
} ?: 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
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
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.SeriesMetadata
|
||||
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.ThumbnailSeriesCollectionRepository
|
||||
import org.gotson.komga.domain.service.SeriesCollectionLifecycle
|
||||
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.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.CollectionUpdateDto
|
||||
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.toDto
|
||||
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.PathVariable
|
||||
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.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 java.util.concurrent.TimeUnit
|
||||
import javax.validation.Valid
|
||||
|
|
@ -57,7 +63,9 @@ private val logger = KotlinLogging.logger {}
|
|||
class SeriesCollectionController(
|
||||
private val collectionRepository: SeriesCollectionRepository,
|
||||
private val collectionLifecycle: SeriesCollectionLifecycle,
|
||||
private val seriesDtoRepository: SeriesDtoRepository
|
||||
private val seriesDtoRepository: SeriesDtoRepository,
|
||||
private val contentDetector: ContentDetector,
|
||||
private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository,
|
||||
) {
|
||||
|
||||
@PageableWithoutSortAsQueryParam
|
||||
|
|
@ -112,6 +120,84 @@ class SeriesCollectionController(
|
|||
} ?: 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
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
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.TaskQueueSseDto
|
||||
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.springframework.http.MediaType
|
||||
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.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.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.DirectoryNotFoundException
|
||||
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.ReadList
|
||||
import org.gotson.komga.domain.model.ScanResult
|
||||
|
|
@ -577,8 +578,8 @@ class LibraryContentLifecycleTest(
|
|||
bookRepository.findByIdOrNull(book.id)?.let {
|
||||
bookRepository.update(it.copy(fileHash = "sameHash"))
|
||||
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(url = URL("file:/sidecar"), type = ThumbnailBook.Type.SIDECAR, 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), MarkSelectedPreference.NO)
|
||||
}
|
||||
|
||||
every { mockHasher.computeHash(any()) } returns "sameHash"
|
||||
|
|
@ -623,8 +624,8 @@ class LibraryContentLifecycleTest(
|
|||
bookRepository.findByIdOrNull(book.id)?.let {
|
||||
bookRepository.update(it.copy(fileHash = "sameHash"))
|
||||
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(url = URL("file:/sidecar"), type = ThumbnailBook.Type.SIDECAR, 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), MarkSelectedPreference.NO)
|
||||
}
|
||||
|
||||
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.BookPage
|
||||
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.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
|
|
@ -474,7 +475,8 @@ class BookControllerTest(
|
|||
thumbnail = Random.nextBytes(100),
|
||||
bookId = book.id,
|
||||
type = ThumbnailBook.Type.GENERATED
|
||||
)
|
||||
),
|
||||
MarkSelectedPreference.YES
|
||||
)
|
||||
|
||||
val url = "/api/v1/books/${book.id}/thumbnail"
|
||||
|
|
@ -533,7 +535,8 @@ class BookControllerTest(
|
|||
thumbnail = Random.nextBytes(1),
|
||||
bookId = book.id,
|
||||
type = ThumbnailBook.Type.GENERATED
|
||||
)
|
||||
),
|
||||
MarkSelectedPreference.YES
|
||||
)
|
||||
|
||||
val url = "/api/v1/books/${book.id}/thumbnail"
|
||||
|
|
@ -546,7 +549,8 @@ class BookControllerTest(
|
|||
thumbnail = Random.nextBytes(1),
|
||||
bookId = book.id,
|
||||
type = ThumbnailBook.Type.GENERATED
|
||||
)
|
||||
),
|
||||
MarkSelectedPreference.YES
|
||||
)
|
||||
|
||||
mockMvc.get(url) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package org.gotson.komga.interfaces.api.rest
|
|||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
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.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
|
|
@ -580,7 +581,8 @@ class SeriesControllerTest(
|
|||
thumbnail = Random.nextBytes(1),
|
||||
bookId = book.id,
|
||||
type = ThumbnailBook.Type.GENERATED
|
||||
)
|
||||
),
|
||||
MarkSelectedPreference.YES
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -614,7 +616,8 @@ class SeriesControllerTest(
|
|||
thumbnail = Random.nextBytes(1),
|
||||
bookId = book.id,
|
||||
type = ThumbnailBook.Type.GENERATED
|
||||
)
|
||||
),
|
||||
MarkSelectedPreference.YES
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue