diff --git a/ERRORCODES.md b/ERRORCODES.md index dd2a9d9a0..6b599b829 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -21,3 +21,8 @@ ERR_1014 | No match for book number within series ERR_1015 | Error while deserializing ComicRack ReadingList ERR_1016 | Directory not accessible or not a directory ERR_1017 | Cannot scan folder that is part of an existing library +ERR_1018 | File not found +ERR_1019 | Cannot import file that is part of an existing library +ERR_1020 | Book to upgrade does not belong to provided series +ERR_1021 | Destination file already exists +ERR_1022 | Newly imported book could not be scanned diff --git a/komga/src/main/kotlin/org/gotson/komga/application/events/EventPublisher.kt b/komga/src/main/kotlin/org/gotson/komga/application/events/EventPublisher.kt new file mode 100644 index 000000000..e3b99961a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/application/events/EventPublisher.kt @@ -0,0 +1,26 @@ +package org.gotson.komga.application.events + +import org.gotson.komga.domain.model.DomainEvent +import org.gotson.komga.infrastructure.jms.QUEUE_SSE +import org.gotson.komga.infrastructure.jms.QUEUE_SSE_TYPE +import org.gotson.komga.infrastructure.jms.QUEUE_TYPE +import org.springframework.jms.core.JmsTemplate +import org.springframework.stereotype.Service +import javax.jms.ConnectionFactory + +@Service +class EventPublisher( + connectionFactory: ConnectionFactory, +) { + private val jmsTemplate = JmsTemplate(connectionFactory).apply { + isPubSubDomain = true + } + + fun publishEvent(event: DomainEvent) { + jmsTemplate.convertAndSend(QUEUE_SSE, event) { + it.apply { + setStringProperty(QUEUE_TYPE, QUEUE_SSE_TYPE) + } + } + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Book.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Book.kt index a2d046fc2..66b60db5a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Book.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Book.kt @@ -2,6 +2,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator import com.jakewharton.byteunits.BinaryByteUnit +import java.io.Serializable import java.net.URL import java.nio.file.Path import java.nio.file.Paths @@ -20,8 +21,11 @@ data class Book( override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() { +) : Auditable(), Serializable { + @delegate:Transient val path: Path by lazy { Paths.get(this.url.toURI()) } + + @delegate:Transient val fileSizeHumanReadable: String by lazy { BinaryByteUnit.format(fileSize) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/DomainEvent.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/DomainEvent.kt new file mode 100644 index 000000000..383c1678b --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/DomainEvent.kt @@ -0,0 +1,34 @@ +package org.gotson.komga.domain.model + +import java.io.Serializable +import java.net.URL + +sealed class DomainEvent : Serializable { + + data class LibraryAdded(val library: Library) : DomainEvent() + data class LibraryUpdated(val library: Library) : DomainEvent() + data class LibraryDeleted(val library: Library) : DomainEvent() + + data class SeriesAdded(val series: Series) : DomainEvent() + data class SeriesUpdated(val series: Series) : DomainEvent() + data class SeriesDeleted(val series: Series) : DomainEvent() + + data class BookAdded(val book: Book) : DomainEvent() + data class BookUpdated(val book: Book) : DomainEvent() + data class BookDeleted(val book: Book) : DomainEvent() + data class BookImported(val book: Book?, val sourceFile: URL, val success: Boolean, val message: String? = null) : DomainEvent() + + data class CollectionAdded(val collection: SeriesCollection) : DomainEvent() + data class CollectionUpdated(val collection: SeriesCollection) : DomainEvent() + data class CollectionDeleted(val collection: SeriesCollection) : DomainEvent() + + data class ReadListAdded(val readList: ReadList) : DomainEvent() + data class ReadListUpdated(val readList: ReadList) : DomainEvent() + data class ReadListDeleted(val readList: ReadList) : DomainEvent() + + data class ReadProgressChanged(val progress: ReadProgress) : DomainEvent() + data class ReadProgressDeleted(val progress: ReadProgress) : DomainEvent() + + data class ThumbnailBookAdded(val thumbnail: ThumbnailBook) : DomainEvent() + data class ThumbnailSeriesAdded(val thumbnail: ThumbnailSeries) : DomainEvent() +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt index 7e3bae9c1..d27ae6547 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt @@ -1,6 +1,18 @@ package org.gotson.komga.domain.model -open class CodedException(message: String, val code: String) : Exception(message) +open class CodedException : Exception { + val code: String + + constructor(cause: Throwable, code: String) : super(cause) { + this.code = code + } + + constructor(message: String, code: String) : super(message) { + this.code = code + } +} +fun Exception.withCode(code: String) = CodedException(this, code) + class MediaNotReadyException : Exception() class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code) class ImageConversionException(message: String, code: String = "") : CodedException(message, code) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt index d2ff31b70..7d37de65c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.time.LocalDateTime import javax.validation.constraints.Email import javax.validation.constraints.NotBlank @@ -24,7 +25,7 @@ data class KomgaUser( val id: String = TsidCreator.getTsid256().toString(), override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() { +) : Auditable(), Serializable { fun roles(): Set { val roles = mutableSetOf(ROLE_USER) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt index cfb393bc5..ce1e30b8c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.net.URL import java.nio.file.Path import java.nio.file.Paths @@ -26,7 +27,8 @@ data class Library( override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() { +) : Auditable(), Serializable { + @delegate:Transient val path: Path by lazy { Paths.get(this.root.toURI()) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt index 89f2e7ed7..c0d8e38f8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadList.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.time.LocalDateTime import java.util.SortedMap @@ -18,4 +19,4 @@ data class ReadList( * Indicates that the bookIds have been filtered and is not exhaustive. */ val filtered: Boolean = false -) : Auditable() +) : Auditable(), Serializable diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadProgress.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadProgress.kt index 9ff3abd65..9d8fcb37c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadProgress.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadProgress.kt @@ -1,5 +1,6 @@ package org.gotson.komga.domain.model +import java.io.Serializable import java.time.LocalDateTime data class ReadProgress( @@ -10,4 +11,4 @@ data class ReadProgress( override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() +) : Auditable(), Serializable diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt index c3b1e2863..b9d65c7f5 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Series.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.net.URL import java.nio.file.Path import java.nio.file.Paths @@ -17,7 +18,8 @@ data class Series( override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() { +) : Auditable(), Serializable { + @delegate:Transient val path: Path by lazy { Paths.get(this.url.toURI()) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt index 6ad54bf0f..f4b5ecc93 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesCollection.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.time.LocalDateTime data class SeriesCollection( @@ -18,4 +19,4 @@ data class SeriesCollection( * Indicates that the seriesIds have been filtered and is not exhaustive. */ val filtered: Boolean = false -) : Auditable() +) : Auditable(), Serializable diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt index e9a7b31ec..9ef2b6fab 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailBook.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.net.URL import java.time.LocalDateTime @@ -15,7 +16,7 @@ data class ThumbnailBook( override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() { +) : Auditable(), Serializable { enum class Type { GENERATED, SIDECAR } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt index 282145226..e9dc316fc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ThumbnailSeries.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model import com.github.f4b6a3.tsid.TsidCreator +import java.io.Serializable import java.net.URL import java.time.LocalDateTime @@ -13,4 +14,4 @@ data class ThumbnailSeries( override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() -) : Auditable() +) : Auditable(), Serializable diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookRepository.kt index 1123c83d0..2325f25aa 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookRepository.kt @@ -13,10 +13,12 @@ interface BookRepository { fun findAll(): Collection fun findAllBySeriesId(seriesId: String): Collection + fun findAllBySeriesIds(seriesIds: Collection): Collection fun findAll(bookSearch: BookSearch): Collection fun findAll(bookSearch: BookSearch, pageable: Pageable): Page fun getLibraryIdOrNull(bookId: String): String? + fun getSeriesIdOrNull(bookId: String): String? fun findFirstIdInSeriesOrNull(seriesId: String): String? fun findAllIdsBySeriesId(seriesId: String): Collection diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt index 7760d9958..63643b897 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt @@ -8,6 +8,7 @@ interface ReadProgressRepository { fun findAll(): Collection fun findAllByUserId(userId: String): Collection fun findAllByBookId(bookId: String): Collection + fun findAllByBookIdsAndUserId(bookIds: Collection, userId: String): Collection fun save(readProgress: ReadProgress) fun save(readProgresses: Collection) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt index f4a4135e7..81bbb9e6a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt @@ -1,11 +1,15 @@ package org.gotson.komga.domain.service import mu.KotlinLogging +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.domain.model.Book +import org.gotson.komga.domain.model.CodedException import org.gotson.komga.domain.model.CopyMode +import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.PathContainedInPath import org.gotson.komga.domain.model.Series +import org.gotson.komga.domain.model.withCode import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.LibraryRepository @@ -41,111 +45,120 @@ class BookImporter( private val readProgressRepository: ReadProgressRepository, private val readListRepository: ReadListRepository, private val libraryRepository: LibraryRepository, + private val eventPublisher: EventPublisher, ) { fun importBook(sourceFile: Path, series: Series, copyMode: CopyMode, destinationName: String? = null, upgradeBookId: String? = null): Book { - if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile") + try { + if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile").withCode("ERR_1018") - libraryRepository.findAll().forEach { library -> - if (sourceFile.startsWith(library.path)) throw PathContainedInPath("Cannot import file that is part of an existing library") - } + libraryRepository.findAll().forEach { library -> + if (sourceFile.startsWith(library.path)) throw PathContainedInPath("Cannot import file that is part of an existing library", "ERR_1019") + } - val destFile = series.path.resolve( - if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").fileName.toString() - else sourceFile.fileName.toString() - ) + val destFile = series.path.resolve( + if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").fileName.toString() + else sourceFile.fileName.toString() + ) - val upgradedBookId = - if (upgradeBookId != null) { - bookRepository.findByIdOrNull(upgradeBookId)?.let { - if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series") - it.id + val upgradedBook = + if (upgradeBookId != null) { + bookRepository.findByIdOrNull(upgradeBookId)?.let { + if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series").withCode("ERR_1020") + it + } + } else null + val upgradedBookPath = + if (upgradedBook != null) + bookRepository.findByIdOrNull(upgradedBook.id)?.path + else null + + var deletedUpgradedFile = false + when { + upgradedBookPath != null && destFile == upgradedBookPath -> { + logger.info { "Deleting existing file: $upgradedBookPath" } + try { + upgradedBookPath.deleteExisting() + deletedUpgradedFile = true + } catch (e: NoSuchFileException) { + logger.warn { "Could not delete upgraded book: $upgradedBookPath" } + } } - } else null - val upgradedBookPath = - if (upgradedBookId != null) - bookRepository.findByIdOrNull(upgradedBookId)?.path - else null + destFile.exists() -> throw FileAlreadyExistsException("Destination file already exists: $destFile").withCode("ERR_1021") + } - var deletedUpgradedFile = false - when { - upgradedBookPath != null && destFile == upgradedBookPath -> { - logger.info { "Deleting existing file: $upgradedBookPath" } - try { - upgradedBookPath.deleteExisting() - deletedUpgradedFile = true - } catch (e: NoSuchFileException) { - logger.warn { "Could not delete upgraded book: $upgradedBookPath" } + when (copyMode) { + CopyMode.MOVE -> { + logger.info { "Moving file $sourceFile to $destFile" } + sourceFile.moveTo(destFile) + } + CopyMode.COPY -> { + logger.info { "Copying file $sourceFile to $destFile" } + sourceFile.copyTo(destFile) + } + CopyMode.HARDLINK -> try { + logger.info { "Hardlink file $sourceFile to $destFile" } + Files.createLink(destFile, sourceFile) + } catch (e: Exception) { + logger.warn(e) { "Filesystem does not support hardlinks, copying instead" } + sourceFile.copyTo(destFile) } } - destFile.exists() -> throw FileAlreadyExistsException("Destination file already exists: $destFile") - } - when (copyMode) { - CopyMode.MOVE -> { - logger.info { "Moving file $sourceFile to $destFile" } - sourceFile.moveTo(destFile) - } - CopyMode.COPY -> { - logger.info { "Copying file $sourceFile to $destFile" } - sourceFile.copyTo(destFile) - } - CopyMode.HARDLINK -> try { - logger.info { "Hardlink file $sourceFile to $destFile" } - Files.createLink(destFile, sourceFile) - } catch (e: Exception) { - logger.warn(e) { "Filesystem does not support hardlinks, copying instead" } - sourceFile.copyTo(destFile) - } - } + val importedBook = fileSystemScanner.scanFile(destFile) + ?.copy(libraryId = series.libraryId) + ?: throw IllegalStateException("Newly imported book could not be scanned: $destFile").withCode("ERR_1022") - val importedBook = fileSystemScanner.scanFile(destFile) - ?.copy(libraryId = series.libraryId) - ?: throw IllegalStateException("Newly imported book could not be scanned: $destFile") + seriesLifecycle.addBooks(series, listOf(importedBook)) - seriesLifecycle.addBooks(series, listOf(importedBook)) - - if (upgradedBookId != null) { - // copy media and mark it as outdated - mediaRepository.findById(upgradedBookId).let { - mediaRepository.update( - it.copy( - bookId = importedBook.id, - status = Media.Status.OUTDATED, - ) - ) - } - - // copy metadata - metadataRepository.findById(upgradedBookId).let { - metadataRepository.update(it.copy(bookId = importedBook.id)) - } - - // copy read progress - readProgressRepository.findAllByBookId(upgradedBookId) - .map { it.copy(bookId = importedBook.id) } - .forEach { readProgressRepository.save(it) } - - // replace upgraded book by imported book in read lists - readListRepository.findAllContainingBookId(upgradedBookId, filterOnLibraryIds = null) - .forEach { rl -> - readListRepository.update( - rl.copy( - bookIds = rl.bookIds.values.map { if (it == upgradedBookId) importedBook.id else it }.toIndexedMap() + if (upgradedBook != null) { + // copy media and mark it as outdated + mediaRepository.findById(upgradedBook.id).let { + mediaRepository.update( + it.copy( + bookId = importedBook.id, + status = Media.Status.OUTDATED, ) ) } - // delete upgraded book file on disk if it has not been replaced earlier - if (upgradedBookPath != null && !deletedUpgradedFile && upgradedBookPath.deleteIfExists()) - logger.info { "Deleted existing file: $upgradedBookPath" } + // copy metadata + metadataRepository.findById(upgradedBook.id).let { + metadataRepository.update(it.copy(bookId = importedBook.id)) + } - // delete upgraded book - bookLifecycle.deleteOne(upgradedBookId) + // copy read progress + readProgressRepository.findAllByBookId(upgradedBook.id) + .map { it.copy(bookId = importedBook.id) } + .forEach { readProgressRepository.save(it) } + + // replace upgraded book by imported book in read lists + readListRepository.findAllContainingBookId(upgradedBook.id, filterOnLibraryIds = null) + .forEach { rl -> + readListRepository.update( + rl.copy( + bookIds = rl.bookIds.values.map { if (it == upgradedBook.id) importedBook.id else it }.toIndexedMap() + ) + ) + } + + // delete upgraded book file on disk if it has not been replaced earlier + if (upgradedBookPath != null && !deletedUpgradedFile && upgradedBookPath.deleteIfExists()) + logger.info { "Deleted existing file: $upgradedBookPath" } + + // delete upgraded book + bookLifecycle.deleteOne(upgradedBook) + } + + seriesLifecycle.sortBooks(series) + + eventPublisher.publishEvent(DomainEvent.BookImported(importedBook, sourceFile.toUri().toURL(), success = true)) + + return importedBook + } catch (e: Exception) { + val msg = if (e is CodedException) e.code else e.message + eventPublisher.publishEvent(DomainEvent.BookImported(null, sourceFile.toUri().toURL(), success = false, msg)) + throw e } - - seriesLifecycle.sortBooks(series) - - return importedBook } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt index ed82a4c52..ab6d840cd 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt @@ -1,9 +1,11 @@ package org.gotson.komga.domain.service import mu.KotlinLogging +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookPageContent 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.Media @@ -34,7 +36,8 @@ class BookLifecycle( private val thumbnailBookRepository: ThumbnailBookRepository, private val readListRepository: ReadListRepository, private val bookAnalyzer: BookAnalyzer, - private val imageConverter: ImageConverter + private val imageConverter: ImageConverter, + private val eventPublisher: EventPublisher, ) { fun analyzeAndPersist(book: Book): Boolean { @@ -49,6 +52,9 @@ class BookLifecycle( } mediaRepository.update(media) + + eventPublisher.publishEvent(DomainEvent.BookUpdated(book)) + return media.status == Media.Status.READY } @@ -79,6 +85,8 @@ class BookLifecycle( } } + eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail)) + if (thumbnail.selected) thumbnailBookRepository.markSelected(thumbnail) else @@ -187,21 +195,24 @@ class BookLifecycle( } } - fun deleteOne(bookId: String) { - logger.info { "Delete book id: $bookId" } + fun deleteOne(book: Book) { + logger.info { "Delete book id: ${book.id}" } - readProgressRepository.deleteByBookId(bookId) - readListRepository.removeBookFromAll(bookId) + readProgressRepository.deleteByBookId(book.id) + readListRepository.removeBookFromAll(book.id) - mediaRepository.delete(bookId) - thumbnailBookRepository.deleteByBookId(bookId) - bookMetadataRepository.delete(bookId) + mediaRepository.delete(book.id) + thumbnailBookRepository.deleteByBookId(book.id) + bookMetadataRepository.delete(book.id) - bookRepository.delete(bookId) + bookRepository.delete(book.id) + + eventPublisher.publishEvent(DomainEvent.BookDeleted(book)) } - fun deleteMany(bookIds: Collection) { - logger.info { "Delete all books: $bookIds" } + fun deleteMany(books: Collection) { + val bookIds = books.map { it.id } + logger.info { "Delete book ids: $bookIds" } readProgressRepository.deleteByBookIds(bookIds) readListRepository.removeBooksFromAll(bookIds) @@ -211,22 +222,31 @@ class BookLifecycle( bookMetadataRepository.delete(bookIds) bookRepository.delete(bookIds) + + books.forEach { eventPublisher.publishEvent(DomainEvent.BookDeleted(it)) } } fun markReadProgress(book: Book, user: KomgaUser, page: Int) { val pages = mediaRepository.getPagesSize(book.id) require(page in 1..pages) { "Page argument ($page) must be within 1 and book page count ($pages)" } - readProgressRepository.save(ReadProgress(book.id, user.id, page, page == pages)) + val progress = ReadProgress(book.id, user.id, page, page == pages) + readProgressRepository.save(progress) + eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(progress)) } fun markReadProgressCompleted(bookId: String, user: KomgaUser) { val media = mediaRepository.findById(bookId) - readProgressRepository.save(ReadProgress(bookId, user.id, media.pages.size, true)) + val progress = ReadProgress(bookId, user.id, media.pages.size, true) + readProgressRepository.save(progress) + eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(progress)) } - fun deleteReadProgress(bookId: String, user: KomgaUser) { - readProgressRepository.delete(bookId, user.id) + fun deleteReadProgress(book: Book, user: KomgaUser) { + readProgressRepository.findByBookIdAndUserIdOrNull(book.id, user.id)?.let { progress -> + readProgressRepository.delete(book.id, user.id) + eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(progress)) + } } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycle.kt index e18f5289d..510142aad 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycle.kt @@ -48,14 +48,14 @@ class LibraryContentLifecycle( // delete series that don't exist anymore if (scannedSeries.isEmpty()) { logger.info { "Scan returned no series, deleting all existing series" } - val seriesIds = seriesRepository.findAllByLibraryId(library.id).map { it.id } - seriesLifecycle.deleteMany(seriesIds) + val series = seriesRepository.findAllByLibraryId(library.id) + seriesLifecycle.deleteMany(series) } else { scannedSeries.keys.map { it.url }.let { urls -> val series = seriesRepository.findAllByLibraryIdAndUrlNotIn(library.id, urls) if (series.isNotEmpty()) { logger.info { "Deleting series not on disk anymore: $series" } - seriesLifecycle.deleteMany(series.map { it.id }) + seriesLifecycle.deleteMany(series) } } } @@ -106,7 +106,7 @@ class LibraryContentLifecycle( .filterNot { existingBook -> newBooksUrls.contains(existingBook.url) } .let { books -> logger.info { "Deleting books not on disk anymore: $books" } - bookLifecycle.deleteMany(books.map { it.id }) + bookLifecycle.deleteMany(books) books.map { it.seriesId }.distinct().forEach { taskReceiver.refreshSeriesMetadata(it) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt index e171620b5..f1ee5afb0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt @@ -1,8 +1,10 @@ package org.gotson.komga.domain.service import mu.KotlinLogging +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.DirectoryNotFoundException +import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.DuplicateNameException import org.gotson.komga.domain.model.Library import org.gotson.komga.domain.model.PathContainedInPath @@ -21,7 +23,8 @@ class LibraryLifecycle( private val seriesLifecycle: SeriesLifecycle, private val seriesRepository: SeriesRepository, private val sidecarRepository: SidecarRepository, - private val taskReceiver: TaskReceiver + private val taskReceiver: TaskReceiver, + private val eventPublisher: EventPublisher, ) { @Throws( @@ -39,6 +42,8 @@ class LibraryLifecycle( libraryRepository.insert(library) taskReceiver.scanLibrary(library.id) + eventPublisher.publishEvent(DomainEvent.LibraryAdded(library)) + return libraryRepository.findById(library.id) } @@ -50,6 +55,8 @@ class LibraryLifecycle( libraryRepository.update(toUpdate) taskReceiver.scanLibrary(toUpdate.id) + + eventPublisher.publishEvent(DomainEvent.LibraryUpdated(toUpdate)) } private fun checkLibraryValidity(library: Library, existing: Collection) { @@ -73,10 +80,12 @@ class LibraryLifecycle( fun deleteLibrary(library: Library) { logger.info { "Deleting library: $library" } - val seriesIds = seriesRepository.findAllByLibraryId(library.id).map { it.id } - seriesLifecycle.deleteMany(seriesIds) + val series = seriesRepository.findAllByLibraryId(library.id) + seriesLifecycle.deleteMany(series) sidecarRepository.deleteByLibraryId(library.id) libraryRepository.delete(library.id) + + eventPublisher.publishEvent(DomainEvent.LibraryDeleted(library)) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt index eadbeb768..c1743e8ae 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt @@ -1,10 +1,12 @@ package org.gotson.komga.domain.service import mu.KotlinLogging +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatch import org.gotson.komga.domain.model.BookMetadataPatchCapability import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.Series import org.gotson.komga.domain.model.SeriesCollection @@ -42,6 +44,7 @@ class MetadataLifecycle( private val collectionLifecycle: SeriesCollectionLifecycle, private val readListRepository: ReadListRepository, private val readListLifecycle: ReadListLifecycle, + private val eventPublisher: EventPublisher, ) { fun refreshMetadata(book: Book, capabilities: List) { @@ -49,6 +52,7 @@ class MetadataLifecycle( val media = mediaRepository.findById(book.id) val library = libraryRepository.findById(book.libraryId) + var changed = false bookMetadataProviders.forEach { provider -> when { @@ -70,6 +74,7 @@ class MetadataLifecycle( (provider is IsbnBarcodeProvider && library.importBarcodeIsbn) ) { handlePatchForBookMetadata(patch, book) + changed = true } if (provider is ComicInfoProvider && library.importComicInfoReadList) { @@ -78,6 +83,8 @@ class MetadataLifecycle( } } } + + if (changed) eventPublisher.publishEvent(DomainEvent.BookUpdated(book)) } private fun handlePatchForReadLists( @@ -138,6 +145,7 @@ class MetadataLifecycle( logger.info { "Refresh metadata for series: $series" } val library = libraryRepository.findById(series.libraryId) + var changed = false seriesMetadataProviders.forEach { provider -> when { @@ -153,6 +161,7 @@ class MetadataLifecycle( (provider is EpubMetadataProvider && library.importEpubSeries) ) { handlePatchForSeriesMetadata(patches, series) + changed = true } if (provider is ComicInfoProvider && library.importComicInfoCollection) { @@ -161,6 +170,8 @@ class MetadataLifecycle( } } } + + if (changed) eventPublisher.publishEvent(DomainEvent.SeriesUpdated(series)) } private fun handlePatchForCollections( @@ -226,6 +237,8 @@ class MetadataLifecycle( val aggregation = metadataAggregator.aggregate(metadatas).copy(seriesId = series.id) bookMetadataAggregationRepository.update(aggregation) + + eventPublisher.publishEvent(DomainEvent.SeriesUpdated(series)) } private fun Iterable.mostFrequent(transform: (T) -> R?): R? { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt index f18031d50..db0fd0d56 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt @@ -1,6 +1,8 @@ package org.gotson.komga.domain.service import mu.KotlinLogging +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.ReadList import org.gotson.komga.domain.model.ReadListRequestResult @@ -18,6 +20,7 @@ class ReadListLifecycle( private val mosaicGenerator: MosaicGenerator, private val readListMatcher: ReadListMatcher, private val readListProvider: ReadListProvider, + private val eventPublisher: EventPublisher, ) { @Throws( @@ -31,6 +34,8 @@ class ReadListLifecycle( readListRepository.insert(readList) + eventPublisher.publishEvent(DomainEvent.ReadListAdded(readList)) + return readListRepository.findByIdOrNull(readList.id)!! } @@ -43,10 +48,14 @@ class ReadListLifecycle( throw DuplicateNameException("Read list name already exists") readListRepository.update(toUpdate) + + eventPublisher.publishEvent(DomainEvent.ReadListUpdated(toUpdate)) } - fun deleteReadList(readListId: String) { - readListRepository.delete(readListId) + fun deleteReadList(readList: ReadList) { + readListRepository.delete(readList.id) + + eventPublisher.publishEvent(DomainEvent.ReadListDeleted(readList)) } fun getThumbnailBytes(readList: ReadList): ByteArray { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt index 45116b80d..10acc2f87 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt @@ -1,6 +1,8 @@ package org.gotson.komga.domain.service import mu.KotlinLogging +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.persistence.SeriesCollectionRepository @@ -13,7 +15,8 @@ private val logger = KotlinLogging.logger {} class SeriesCollectionLifecycle( private val collectionRepository: SeriesCollectionRepository, private val seriesLifecycle: SeriesLifecycle, - private val mosaicGenerator: MosaicGenerator + private val mosaicGenerator: MosaicGenerator, + private val eventPublisher: EventPublisher, ) { @Throws( @@ -27,6 +30,8 @@ class SeriesCollectionLifecycle( collectionRepository.insert(collection) + eventPublisher.publishEvent(DomainEvent.CollectionAdded(collection)) + return collectionRepository.findByIdOrNull(collection.id)!! } @@ -40,10 +45,13 @@ class SeriesCollectionLifecycle( throw DuplicateNameException("Collection name already exists") collectionRepository.update(toUpdate) + + eventPublisher.publishEvent(DomainEvent.CollectionUpdated(toUpdate)) } - fun deleteCollection(collectionId: String) { - collectionRepository.delete(collectionId) + fun deleteCollection(collection: SeriesCollection) { + collectionRepository.delete(collection.id) + eventPublisher.publishEvent(DomainEvent.CollectionDeleted(collection)) } fun getThumbnailBytes(collection: SeriesCollection): ByteArray { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt index 5bfa9c3a4..9c14b4724 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt @@ -3,11 +3,13 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import org.apache.commons.lang3.StringUtils +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadata import org.gotson.komga.domain.model.BookMetadataAggregation import org.gotson.komga.domain.model.BookMetadataPatchCapability +import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.KomgaUser import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.ReadProgress @@ -43,7 +45,8 @@ class SeriesLifecycle( private val bookMetadataAggregationRepository: BookMetadataAggregationRepository, private val collectionRepository: SeriesCollectionRepository, private val readProgressRepository: ReadProgressRepository, - private val taskReceiver: TaskReceiver + private val taskReceiver: TaskReceiver, + private val eventPublisher: EventPublisher, ) { fun sortBooks(series: Series) { @@ -90,16 +93,15 @@ class SeriesLifecycle( booksToAdd.forEach { check(it.libraryId == series.libraryId) { "Cannot add book to series if they don't share the same libraryId" } } + val toAdd = booksToAdd.map { it.copy(seriesId = series.id) } - bookRepository.insert( - booksToAdd.map { it.copy(seriesId = series.id) } - ) + bookRepository.insert(toAdd) // create associated media - mediaRepository.insert(booksToAdd.map { Media(bookId = it.id) }) + mediaRepository.insert(toAdd.map { Media(bookId = it.id) }) // create associated metadata - booksToAdd.map { + toAdd.map { BookMetadata( title = it.name, number = it.number.toString(), @@ -107,6 +109,8 @@ class SeriesLifecycle( bookId = it.id ) }.let { bookMetadataRepository.insert(it) } + + toAdd.forEach { eventPublisher.publishEvent(DomainEvent.BookAdded(it)) } } fun createSeries(series: Series): Series { @@ -124,28 +128,17 @@ class SeriesLifecycle( BookMetadataAggregation(seriesId = series.id) ) + eventPublisher.publishEvent(DomainEvent.SeriesAdded(series)) + return seriesRepository.findByIdOrNull(series.id)!! } - fun deleteOne(seriesId: String) { - logger.info { "Delete series id: $seriesId" } - - val bookIds = bookRepository.findAllIdsBySeriesId(seriesId) - bookLifecycle.deleteMany(bookIds) - - collectionRepository.removeSeriesFromAll(seriesId) - thumbnailsSeriesRepository.deleteBySeriesId(seriesId) - seriesMetadataRepository.delete(seriesId) - bookMetadataAggregationRepository.delete(seriesId) - - seriesRepository.delete(seriesId) - } - - fun deleteMany(seriesIds: Collection) { + fun deleteMany(series: Collection) { + val seriesIds = series.map { it.id } logger.info { "Delete series ids: $seriesIds" } - val bookIds = bookRepository.findAllIdsBySeriesIds(seriesIds) - bookLifecycle.deleteMany(bookIds) + val books = bookRepository.findAllBySeriesIds(seriesIds) + bookLifecycle.deleteMany(books) collectionRepository.removeSeriesFromAll(seriesIds) thumbnailsSeriesRepository.deleteBySeriesIds(seriesIds) @@ -153,6 +146,8 @@ class SeriesLifecycle( bookMetadataAggregationRepository.delete(seriesIds) seriesRepository.delete(seriesIds) + + series.forEach { eventPublisher.publishEvent(DomainEvent.SeriesDeleted(it)) } } fun markReadProgressCompleted(seriesId: String, user: KomgaUser) { @@ -160,10 +155,15 @@ class SeriesLifecycle( .map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) } readProgressRepository.save(progresses) + progresses.forEach { eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(it)) } } fun deleteReadProgress(seriesId: String, user: KomgaUser) { - readProgressRepository.deleteByBookIdsAndUserId(bookRepository.findAllIdsBySeriesId(seriesId), user.id) + val bookIds = bookRepository.findAllIdsBySeriesId(seriesId) + val progresses = readProgressRepository.findAllByBookIdsAndUserId(bookIds, user.id) + readProgressRepository.deleteByBookIdsAndUserId(bookIds, user.id) + + progresses.forEach { eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(it)) } } fun getThumbnail(seriesId: String): ThumbnailSeries? { @@ -197,6 +197,8 @@ class SeriesLifecycle( } thumbnailsSeriesRepository.insert(thumbnail) + eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail)) + if (thumbnail.selected) thumbnailsSeriesRepository.markSelected(thumbnail) } @@ -224,5 +226,6 @@ class SeriesLifecycle( } } } + private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI())) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jms/ArtemisConfig.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jms/ArtemisConfig.kt index 687360206..176e07fc1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jms/ArtemisConfig.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jms/ArtemisConfig.kt @@ -3,16 +3,27 @@ package org.gotson.komga.infrastructure.jms import org.apache.activemq.artemis.api.core.QueueConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.core.settings.impl.AddressSettings +import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer import org.springframework.boot.autoconfigure.jms.artemis.ArtemisConfigurationCustomizer +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.jms.config.DefaultJmsListenerContainerFactory +import javax.jms.ConnectionFactory import org.apache.activemq.artemis.core.config.Configuration as ArtemisConfiguration const val QUEUE_UNIQUE_ID = "unique_id" const val QUEUE_TYPE = "type" + const val QUEUE_TASKS = "tasks.background" const val QUEUE_TASKS_TYPE = "task" const val QUEUE_TASKS_SELECTOR = "$QUEUE_TYPE = '$QUEUE_TASKS_TYPE'" +const val QUEUE_SSE = "sse" +const val QUEUE_SSE_TYPE = "sse" +const val QUEUE_SSE_SELECTOR = "$QUEUE_TYPE = '$QUEUE_SSE_TYPE'" + +const val TOPIC_FACTORY = "topicJmsListenerContainerFactory" + @Configuration class ArtemisConfig : ArtemisConfigurationCustomizer { override fun customize(configuration: ArtemisConfiguration?) { @@ -32,6 +43,21 @@ class ArtemisConfig : ArtemisConfigurationCustomizer { .setLastValueKey(QUEUE_UNIQUE_ID) .setRoutingType(RoutingType.ANYCAST) ) + it.addQueueConfiguration( + QueueConfiguration(QUEUE_SSE) + .setAddress(QUEUE_SSE) + .setRoutingType(RoutingType.MULTICAST) + ) } } + + @Bean(TOPIC_FACTORY) + fun topicJmsListenerContainerFactory( + connectionFactory: ConnectionFactory, + configurer: DefaultJmsListenerContainerFactoryConfigurer, + ): DefaultJmsListenerContainerFactory = + DefaultJmsListenerContainerFactory().apply { + configurer.configure(this, connectionFactory) + setPubSubDomain(true) + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDao.kt index 60d83d5f4..fec29cc10 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDao.kt @@ -54,6 +54,12 @@ class BookDao( .fetchInto(b) .map { it.toDomain() } + override fun findAllBySeriesIds(seriesIds: Collection): Collection = + dsl.selectFrom(b) + .where(b.SERIES_ID.`in`(seriesIds)) + .fetchInto(b) + .map { it.toDomain() } + override fun findAll(): Collection = dsl.select(*b.fields()) .from(b) @@ -106,6 +112,12 @@ class BookDao( .where(b.ID.eq(bookId)) .fetchOne(b.LIBRARY_ID) + override fun getSeriesIdOrNull(bookId: String): String? = + dsl.select(b.SERIES_ID) + .from(b) + .where(b.ID.eq(bookId)) + .fetchOne(b.SERIES_ID) + override fun findFirstIdInSeriesOrNull(seriesId: String): String? = dsl.select(b.ID) .from(b) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt index 8b479a187..7b6c70eb6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt @@ -43,6 +43,12 @@ class ReadProgressDao( .fetchInto(r) .map { it.toDomain() } + override fun findAllByBookIdsAndUserId(bookIds: Collection, userId: String): Collection = + dsl.selectFrom(r) + .where(r.BOOK_ID.`in`(bookIds).and(r.USER_ID.eq(userId))) + .fetchInto(r) + .map { it.toDomain() } + override fun save(readProgress: ReadProgress) { dsl.transaction { config -> config.dsl().saveQuery(readProgress).execute() diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt index 95fb65ea3..ecce5295a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt @@ -41,7 +41,8 @@ class SecurityConfiguration( // all other endpoints are restricted to authenticated users .antMatchers( "/api/**", - "/opds/**" + "/opds/**", + "/sse/**" ).hasRole(ROLE_USER) .and() diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index f3290d71c..ef61f51ac 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -7,11 +7,13 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import mu.KotlinLogging import org.apache.commons.io.IOUtils +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.application.tasks.HIGHEST_PRIORITY import org.gotson.komga.application.tasks.HIGH_PRIORITY import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.BookSearchWithReadProgress +import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.ImageConversionException import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException @@ -86,6 +88,7 @@ class BookController( private val bookDtoRepository: BookDtoRepository, private val readListRepository: ReadListRepository, private val contentDetector: ContentDetector, + private val eventPublisher: EventPublisher, ) { @PageableAsQueryParam @@ -471,6 +474,8 @@ class BookController( } bookMetadataRepository.update(updated) taskReceiver.aggregateSeriesMetadata(bookRepository.findByIdOrNull(bookId)!!.seriesId) + + bookRepository.findByIdOrNull(bookId)?.let { eventPublisher.publishEvent(DomainEvent.BookUpdated(it)) } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) @PatchMapping("api/v1/books/{bookId}/read-progress") @@ -504,7 +509,7 @@ class BookController( bookRepository.findByIdOrNull(bookId)?.let { book -> if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN) - bookLifecycle.deleteReadProgress(book.id, principal.user) + bookLifecycle.deleteReadProgress(book, principal.user) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt index 10da4a3fd..d0bc296cd 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt @@ -179,7 +179,7 @@ class ReadListController( @PathVariable id: String ) { readListRepository.findByIdOrNull(id)?.let { - readListLifecycle.deleteReadList(it.id) + readListLifecycle.deleteReadList(it) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt index 0bc1ccd5b..9c689e0a6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt @@ -152,7 +152,7 @@ class SeriesCollectionController( @PathVariable id: String ) { collectionRepository.findByIdOrNull(id)?.let { - collectionLifecycle.deleteCollection(it.id) + collectionLifecycle.deleteCollection(it) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt index 0f99b0fa6..7affb069e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt @@ -9,10 +9,12 @@ import mu.KotlinLogging import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream import org.apache.commons.io.IOUtils +import org.gotson.komga.application.events.EventPublisher import org.gotson.komga.application.tasks.HIGH_PRIORITY import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.BookSearchWithReadProgress +import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.ROLE_ADMIN import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD @@ -86,6 +88,7 @@ class SeriesController( private val bookDtoRepository: BookDtoRepository, private val collectionRepository: SeriesCollectionRepository, private val readProgressDtoRepository: ReadProgressDtoRepository, + private val eventPublisher: EventPublisher, ) { @PageableAsQueryParam @@ -353,6 +356,8 @@ class SeriesController( ) } seriesMetadataRepository.update(updated) + + seriesRepository.findByIdOrNull(seriesId)?.let { eventPublisher.publishEvent(DomainEvent.SeriesUpdated(it)) } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) @PostMapping("{seriesId}/read-progress") diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt new file mode 100644 index 000000000..94845fcdb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt @@ -0,0 +1,117 @@ +package org.gotson.komga.interfaces.sse + +import mu.KotlinLogging +import org.gotson.komga.domain.model.DomainEvent +import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.infrastructure.jms.QUEUE_SSE +import org.gotson.komga.infrastructure.jms.QUEUE_SSE_SELECTOR +import org.gotson.komga.infrastructure.jms.QUEUE_TASKS +import org.gotson.komga.infrastructure.jms.TOPIC_FACTORY +import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.web.toFilePath +import org.gotson.komga.interfaces.sse.dto.BookImportSseDto +import org.gotson.komga.interfaces.sse.dto.BookSseDto +import org.gotson.komga.interfaces.sse.dto.CollectionSseDto +import org.gotson.komga.interfaces.sse.dto.LibrarySseDto +import org.gotson.komga.interfaces.sse.dto.ReadListSseDto +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.ThumbnailSeriesSseDto +import org.springframework.http.MediaType +import org.springframework.jms.annotation.JmsListener +import org.springframework.jms.core.JmsTemplate +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter +import java.io.IOException +import java.util.Collections +import javax.jms.QueueBrowser +import javax.jms.Session + +private val logger = KotlinLogging.logger {} + +@Controller +class SseController( + private val bookRepository: BookRepository, + private val jmsTemplate: JmsTemplate, +) { + + private val emitters = Collections.synchronizedMap(HashMap()) + + @GetMapping("sse/v1/events") + fun sse( + @AuthenticationPrincipal principal: KomgaPrincipal, + ): SseEmitter { + val emitter = SseEmitter() + emitter.onCompletion { synchronized(emitters) { emitters.remove(emitter) } } + emitter.onTimeout { emitter.complete() } + emitters[emitter] = principal.user + return emitter + } + + @Scheduled(fixedRate = 10_000) + fun taskCount() { + val size = jmsTemplate.browse(QUEUE_TASKS) { _: Session, browser: QueueBrowser -> + browser.enumeration.toList().size + } ?: 0 + + emitSse("TaskQueueStatus", TaskQueueSseDto(size), adminOnly = true) + } + + @JmsListener(destination = QUEUE_SSE, selector = QUEUE_SSE_SELECTOR, containerFactory = TOPIC_FACTORY) + fun handleSseEvent(event: DomainEvent) { + when (event) { + is DomainEvent.LibraryAdded -> emitSse("LibraryAdded", LibrarySseDto(event.library.id)) + is DomainEvent.LibraryUpdated -> emitSse("LibraryChanged", LibrarySseDto(event.library.id)) + is DomainEvent.LibraryDeleted -> emitSse("LibraryDeleted", LibrarySseDto(event.library.id)) + + is DomainEvent.SeriesAdded -> emitSse("SeriesAdded", SeriesSseDto(event.series.id, event.series.libraryId)) + is DomainEvent.SeriesUpdated -> emitSse("SeriesChanged", SeriesSseDto(event.series.id, event.series.libraryId)) + is DomainEvent.SeriesDeleted -> emitSse("SeriesDeleted", SeriesSseDto(event.series.id, event.series.libraryId)) + + is DomainEvent.BookAdded -> emitSse("BookAdded", BookSseDto(event.book.id, event.book.seriesId, event.book.libraryId)) + is DomainEvent.BookUpdated -> emitSse("BookChanged", BookSseDto(event.book.id, event.book.seriesId, event.book.libraryId)) + is DomainEvent.BookDeleted -> emitSse("BookDeleted", BookSseDto(event.book.id, event.book.seriesId, event.book.libraryId)) + is DomainEvent.BookImported -> emitSse("BookImported", BookImportSseDto(event.book?.id, event.sourceFile.toFilePath(), event.success, event.message), adminOnly = true) + + is DomainEvent.ReadListAdded -> emitSse("ReadListAdded", ReadListSseDto(event.readList.id, event.readList.bookIds.map { it.value })) + is DomainEvent.ReadListUpdated -> emitSse("ReadListChanged", ReadListSseDto(event.readList.id, event.readList.bookIds.map { it.value })) + is DomainEvent.ReadListDeleted -> emitSse("ReadListDeleted", ReadListSseDto(event.readList.id, event.readList.bookIds.map { it.value })) + + is DomainEvent.CollectionAdded -> emitSse("CollectionAdded", CollectionSseDto(event.collection.id, event.collection.seriesIds)) + is DomainEvent.CollectionUpdated -> emitSse("CollectionChanged", CollectionSseDto(event.collection.id, event.collection.seriesIds)) + is DomainEvent.CollectionDeleted -> emitSse("CollectionDeleted", CollectionSseDto(event.collection.id, event.collection.seriesIds)) + + is DomainEvent.ReadProgressChanged -> emitSse("ReadProgressChanged", ReadProgressSseDto(event.progress.bookId, event.progress.userId), userIdOnly = event.progress.userId) + is DomainEvent.ReadProgressDeleted -> emitSse("ReadProgressDeleted", ReadProgressSseDto(event.progress.bookId, event.progress.userId), userIdOnly = event.progress.userId) + + is DomainEvent.ThumbnailBookAdded -> emitSse("ThumbnailBookAdded", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty())) + is DomainEvent.ThumbnailSeriesAdded -> emitSse("ThumbnailSeriesAdded", ThumbnailSeriesSseDto(event.thumbnail.seriesId)) + } + } + + private fun emitSse(name: String, data: Any, adminOnly: Boolean = false, userIdOnly: String? = null) { + logger.debug { "Publish SSE: '$name':$data" } + + synchronized(emitters) { + emitters + .filter { if (adminOnly) it.value.roleAdmin else true } + .filter { if (userIdOnly != null) it.value.id == userIdOnly else true } + .forEach { (emitter, _) -> + try { + emitter.send( + SseEmitter.event() + .name(name) + .data(data, MediaType.APPLICATION_JSON) + ) + } catch (e: IOException) { + } + } + } + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/BookImportSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/BookImportSseDto.kt new file mode 100644 index 000000000..d37a95474 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/BookImportSseDto.kt @@ -0,0 +1,8 @@ +package org.gotson.komga.interfaces.sse.dto + +data class BookImportSseDto( + val bookId: String?, + val sourceFile: String, + val success: Boolean, + val message: String? = null, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/BookSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/BookSseDto.kt new file mode 100644 index 000000000..834d05fea --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/BookSseDto.kt @@ -0,0 +1,7 @@ +package org.gotson.komga.interfaces.sse.dto + +data class BookSseDto( + val bookId: String, + val seriesId: String, + val libraryId: String, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/CollectionSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/CollectionSseDto.kt new file mode 100644 index 000000000..b3d6db3cb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/CollectionSseDto.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.interfaces.sse.dto + +data class CollectionSseDto( + val collectionId: String, + val seriesIds: List, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/LibrarySseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/LibrarySseDto.kt new file mode 100644 index 000000000..afc3aa0ab --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/LibrarySseDto.kt @@ -0,0 +1,5 @@ +package org.gotson.komga.interfaces.sse.dto + +data class LibrarySseDto( + val libraryId: String, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ReadListSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ReadListSseDto.kt new file mode 100644 index 000000000..788b245fb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ReadListSseDto.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.interfaces.sse.dto + +data class ReadListSseDto( + val readListId: String, + val bookIds: List, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ReadProgressSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ReadProgressSseDto.kt new file mode 100644 index 000000000..023c75781 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ReadProgressSseDto.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.interfaces.sse.dto + +data class ReadProgressSseDto( + val bookId: String, + val userId: String, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/SeriesSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/SeriesSseDto.kt new file mode 100644 index 000000000..63620302d --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/SeriesSseDto.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.interfaces.sse.dto + +data class SeriesSseDto( + val seriesId: String, + val libraryId: String, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/TaskQueueSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/TaskQueueSseDto.kt new file mode 100644 index 000000000..d2402857a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/TaskQueueSseDto.kt @@ -0,0 +1,5 @@ +package org.gotson.komga.interfaces.sse.dto + +data class TaskQueueSseDto( + val count: Int, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt new file mode 100644 index 000000000..f0978ffaa --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.interfaces.sse.dto + +data class ThumbnailBookSseDto( + val bookId: String, + val seriesId: String, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt new file mode 100644 index 000000000..5a2a43438 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt @@ -0,0 +1,5 @@ +package org.gotson.komga.interfaces.sse.dto + +data class ThumbnailSeriesSseDto( + val seriesId: String, +) diff --git a/komga/src/main/resources/application-dev.yml b/komga/src/main/resources/application-dev.yml index bd631c2c2..55baf4602 100644 --- a/komga/src/main/resources/application-dev.yml +++ b/komga/src/main/resources/application-dev.yml @@ -2,9 +2,9 @@ komga: remember-me: key: changeMe! validity: 2592000 # 1 month - # libraries-scan-cron: "*/5 * * * * ?" #every 5 seconds +# libraries-scan-cron: "*/5 * * * * ?" #every 5 seconds libraries-scan-cron: "-" #disable - libraries-scan-startup: true + libraries-scan-startup: false database: file: ":memory:" cors.allowed-origins: diff --git a/komga/src/main/resources/application.yml b/komga/src/main/resources/application.yml index 40959d107..b5777d9a6 100644 --- a/komga/src/main/resources/application.yml +++ b/komga/src/main/resources/application.yml @@ -5,7 +5,7 @@ logging: file: name: \${user.home}/.komga/komga.log level: - org.apache.activemq.audit.message: WARN + org.apache.activemq.audit: WARN komga: libraries-scan-cron: "0 */15 * * * ?" diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt index 1b0d14e9b..288b06336 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt @@ -75,7 +75,7 @@ class BookImporterTest( @AfterEach fun `clear repository`() { - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @Test diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt index 89c457dbb..5e2f60884 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt @@ -61,7 +61,7 @@ class BookLifecycleTest( @AfterEach fun `clear repository`() { - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @Test diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt index 3348cb1e3..8eea2c51f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt @@ -49,7 +49,7 @@ class ReadListMatcherTest( @AfterEach fun `clear repository`() { readListRepository.deleteAll() - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @Test diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt index 86604d4d4..d8313738a 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/SeriesLifecycleTest.kt @@ -42,7 +42,7 @@ class SeriesLifecycleTest( @AfterEach fun `clear repository`() { - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @Test @@ -88,7 +88,7 @@ class SeriesLifecycleTest( // when val book = bookRepository.findAllBySeriesId(createdSeries.id).first { it.name == "book 2" } - bookLifecycle.deleteOne(book.id) + bookLifecycle.deleteOne(book) seriesLifecycle.sortBooks(createdSeries) // then diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt index 75adcaf58..8a09f8faf 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDaoTest.kt @@ -54,7 +54,7 @@ class BookDtoDaoTest( @AfterEach fun deleteBooks() { - bookLifecycle.deleteMany(bookRepository.findAll().map { it.id }) + bookLifecycle.deleteMany(bookRepository.findAll()) } @AfterAll diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDaoTest.kt index 3bdffaf6f..707b6430a 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDaoTest.kt @@ -51,7 +51,7 @@ class SeriesDtoDaoTest( @AfterEach fun deleteSeries() { - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @AfterAll diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt index 70fb9b2ab..0a0eeb41d 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt @@ -91,7 +91,7 @@ class BookControllerTest( @AfterEach fun `clear repository`() { - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @Nested diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt index f12a4d847..86bfefdfd 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt @@ -83,7 +83,7 @@ class SeriesControllerTest( @AfterEach fun `clear repository`() { - seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + seriesLifecycle.deleteMany(seriesRepository.findAll()) } @Nested