mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 08:32:25 +01:00
feat(sse): publish server-sent events
This commit is contained in:
parent
b7c2c09ff4
commit
691c7f0071
52 changed files with 565 additions and 164 deletions
|
|
@ -21,3 +21,8 @@ ERR_1014 | No match for book number within series
|
||||||
ERR_1015 | Error while deserializing ComicRack ReadingList
|
ERR_1015 | Error while deserializing ComicRack ReadingList
|
||||||
ERR_1016 | Directory not accessible or not a directory
|
ERR_1016 | Directory not accessible or not a directory
|
||||||
ERR_1017 | Cannot scan folder that is part of an existing library
|
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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
import com.jakewharton.byteunits.BinaryByteUnit
|
import com.jakewharton.byteunits.BinaryByteUnit
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
@ -20,8 +21,11 @@ data class Book(
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable() {
|
) : Auditable(), Serializable {
|
||||||
|
|
||||||
|
@delegate:Transient
|
||||||
val path: Path by lazy { Paths.get(this.url.toURI()) }
|
val path: Path by lazy { Paths.get(this.url.toURI()) }
|
||||||
|
|
||||||
|
@delegate:Transient
|
||||||
val fileSizeHumanReadable: String by lazy { BinaryByteUnit.format(fileSize) }
|
val fileSizeHumanReadable: String by lazy { BinaryByteUnit.format(fileSize) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
package org.gotson.komga.domain.model
|
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 MediaNotReadyException : Exception()
|
||||||
class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code)
|
class MediaUnsupportedException(message: String, code: String = "") : CodedException(message, code)
|
||||||
class ImageConversionException(message: String, code: String = "") : CodedException(message, code)
|
class ImageConversionException(message: String, code: String = "") : CodedException(message, code)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import javax.validation.constraints.Email
|
import javax.validation.constraints.Email
|
||||||
import javax.validation.constraints.NotBlank
|
import javax.validation.constraints.NotBlank
|
||||||
|
|
@ -24,7 +25,7 @@ data class KomgaUser(
|
||||||
val id: String = TsidCreator.getTsid256().toString(),
|
val id: String = TsidCreator.getTsid256().toString(),
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable() {
|
) : Auditable(), Serializable {
|
||||||
|
|
||||||
fun roles(): Set<String> {
|
fun roles(): Set<String> {
|
||||||
val roles = mutableSetOf(ROLE_USER)
|
val roles = mutableSetOf(ROLE_USER)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
@ -26,7 +27,8 @@ data class Library(
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable() {
|
) : Auditable(), Serializable {
|
||||||
|
|
||||||
|
@delegate:Transient
|
||||||
val path: Path by lazy { Paths.get(this.root.toURI()) }
|
val path: Path by lazy { Paths.get(this.root.toURI()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.SortedMap
|
import java.util.SortedMap
|
||||||
|
|
||||||
|
|
@ -18,4 +19,4 @@ data class ReadList(
|
||||||
* Indicates that the bookIds have been filtered and is not exhaustive.
|
* Indicates that the bookIds have been filtered and is not exhaustive.
|
||||||
*/
|
*/
|
||||||
val filtered: Boolean = false
|
val filtered: Boolean = false
|
||||||
) : Auditable()
|
) : Auditable(), Serializable
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
data class ReadProgress(
|
data class ReadProgress(
|
||||||
|
|
@ -10,4 +11,4 @@ data class ReadProgress(
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable()
|
) : Auditable(), Serializable
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
@ -17,7 +18,8 @@ data class Series(
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable() {
|
) : Auditable(), Serializable {
|
||||||
|
|
||||||
|
@delegate:Transient
|
||||||
val path: Path by lazy { Paths.get(this.url.toURI()) }
|
val path: Path by lazy { Paths.get(this.url.toURI()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
data class SeriesCollection(
|
data class SeriesCollection(
|
||||||
|
|
@ -18,4 +19,4 @@ data class SeriesCollection(
|
||||||
* Indicates that the seriesIds have been filtered and is not exhaustive.
|
* Indicates that the seriesIds have been filtered and is not exhaustive.
|
||||||
*/
|
*/
|
||||||
val filtered: Boolean = false
|
val filtered: Boolean = false
|
||||||
) : Auditable()
|
) : Auditable(), Serializable
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ data class ThumbnailBook(
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable() {
|
) : Auditable(), Serializable {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
GENERATED, SIDECAR
|
GENERATED, SIDECAR
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidCreator
|
import com.github.f4b6a3.tsid.TsidCreator
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
|
@ -13,4 +14,4 @@ data class ThumbnailSeries(
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
) : Auditable()
|
) : Auditable(), Serializable
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ interface BookRepository {
|
||||||
|
|
||||||
fun findAll(): Collection<Book>
|
fun findAll(): Collection<Book>
|
||||||
fun findAllBySeriesId(seriesId: String): Collection<Book>
|
fun findAllBySeriesId(seriesId: String): Collection<Book>
|
||||||
|
fun findAllBySeriesIds(seriesIds: Collection<String>): Collection<Book>
|
||||||
fun findAll(bookSearch: BookSearch): Collection<Book>
|
fun findAll(bookSearch: BookSearch): Collection<Book>
|
||||||
fun findAll(bookSearch: BookSearch, pageable: Pageable): Page<Book>
|
fun findAll(bookSearch: BookSearch, pageable: Pageable): Page<Book>
|
||||||
|
|
||||||
fun getLibraryIdOrNull(bookId: String): String?
|
fun getLibraryIdOrNull(bookId: String): String?
|
||||||
|
fun getSeriesIdOrNull(bookId: String): String?
|
||||||
fun findFirstIdInSeriesOrNull(seriesId: String): String?
|
fun findFirstIdInSeriesOrNull(seriesId: String): String?
|
||||||
|
|
||||||
fun findAllIdsBySeriesId(seriesId: String): Collection<String>
|
fun findAllIdsBySeriesId(seriesId: String): Collection<String>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ interface ReadProgressRepository {
|
||||||
fun findAll(): Collection<ReadProgress>
|
fun findAll(): Collection<ReadProgress>
|
||||||
fun findAllByUserId(userId: String): Collection<ReadProgress>
|
fun findAllByUserId(userId: String): Collection<ReadProgress>
|
||||||
fun findAllByBookId(bookId: String): Collection<ReadProgress>
|
fun findAllByBookId(bookId: String): Collection<ReadProgress>
|
||||||
|
fun findAllByBookIdsAndUserId(bookIds: Collection<String>, userId: String): Collection<ReadProgress>
|
||||||
|
|
||||||
fun save(readProgress: ReadProgress)
|
fun save(readProgress: ReadProgress)
|
||||||
fun save(readProgresses: Collection<ReadProgress>)
|
fun save(readProgresses: Collection<ReadProgress>)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.gotson.komga.application.events.EventPublisher
|
||||||
import org.gotson.komga.domain.model.Book
|
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.CopyMode
|
||||||
|
import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
import org.gotson.komga.domain.model.PathContainedInPath
|
import org.gotson.komga.domain.model.PathContainedInPath
|
||||||
import org.gotson.komga.domain.model.Series
|
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.BookMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||||
|
|
@ -41,13 +45,15 @@ class BookImporter(
|
||||||
private val readProgressRepository: ReadProgressRepository,
|
private val readProgressRepository: ReadProgressRepository,
|
||||||
private val readListRepository: ReadListRepository,
|
private val readListRepository: ReadListRepository,
|
||||||
private val libraryRepository: LibraryRepository,
|
private val libraryRepository: LibraryRepository,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun importBook(sourceFile: Path, series: Series, copyMode: CopyMode, destinationName: String? = null, upgradeBookId: String? = null): Book {
|
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 ->
|
libraryRepository.findAll().forEach { library ->
|
||||||
if (sourceFile.startsWith(library.path)) throw PathContainedInPath("Cannot import file that is part of an existing 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(
|
val destFile = series.path.resolve(
|
||||||
|
|
@ -55,16 +61,16 @@ class BookImporter(
|
||||||
else sourceFile.fileName.toString()
|
else sourceFile.fileName.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
val upgradedBookId =
|
val upgradedBook =
|
||||||
if (upgradeBookId != null) {
|
if (upgradeBookId != null) {
|
||||||
bookRepository.findByIdOrNull(upgradeBookId)?.let {
|
bookRepository.findByIdOrNull(upgradeBookId)?.let {
|
||||||
if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series")
|
if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series").withCode("ERR_1020")
|
||||||
it.id
|
it
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
val upgradedBookPath =
|
val upgradedBookPath =
|
||||||
if (upgradedBookId != null)
|
if (upgradedBook != null)
|
||||||
bookRepository.findByIdOrNull(upgradedBookId)?.path
|
bookRepository.findByIdOrNull(upgradedBook.id)?.path
|
||||||
else null
|
else null
|
||||||
|
|
||||||
var deletedUpgradedFile = false
|
var deletedUpgradedFile = false
|
||||||
|
|
@ -78,7 +84,7 @@ class BookImporter(
|
||||||
logger.warn { "Could not delete upgraded book: $upgradedBookPath" }
|
logger.warn { "Could not delete upgraded book: $upgradedBookPath" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
destFile.exists() -> throw FileAlreadyExistsException("Destination file already exists: $destFile")
|
destFile.exists() -> throw FileAlreadyExistsException("Destination file already exists: $destFile").withCode("ERR_1021")
|
||||||
}
|
}
|
||||||
|
|
||||||
when (copyMode) {
|
when (copyMode) {
|
||||||
|
|
@ -101,13 +107,13 @@ class BookImporter(
|
||||||
|
|
||||||
val importedBook = fileSystemScanner.scanFile(destFile)
|
val importedBook = fileSystemScanner.scanFile(destFile)
|
||||||
?.copy(libraryId = series.libraryId)
|
?.copy(libraryId = series.libraryId)
|
||||||
?: throw IllegalStateException("Newly imported book could not be scanned: $destFile")
|
?: throw IllegalStateException("Newly imported book could not be scanned: $destFile").withCode("ERR_1022")
|
||||||
|
|
||||||
seriesLifecycle.addBooks(series, listOf(importedBook))
|
seriesLifecycle.addBooks(series, listOf(importedBook))
|
||||||
|
|
||||||
if (upgradedBookId != null) {
|
if (upgradedBook != null) {
|
||||||
// copy media and mark it as outdated
|
// copy media and mark it as outdated
|
||||||
mediaRepository.findById(upgradedBookId).let {
|
mediaRepository.findById(upgradedBook.id).let {
|
||||||
mediaRepository.update(
|
mediaRepository.update(
|
||||||
it.copy(
|
it.copy(
|
||||||
bookId = importedBook.id,
|
bookId = importedBook.id,
|
||||||
|
|
@ -117,21 +123,21 @@ class BookImporter(
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy metadata
|
// copy metadata
|
||||||
metadataRepository.findById(upgradedBookId).let {
|
metadataRepository.findById(upgradedBook.id).let {
|
||||||
metadataRepository.update(it.copy(bookId = importedBook.id))
|
metadataRepository.update(it.copy(bookId = importedBook.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy read progress
|
// copy read progress
|
||||||
readProgressRepository.findAllByBookId(upgradedBookId)
|
readProgressRepository.findAllByBookId(upgradedBook.id)
|
||||||
.map { it.copy(bookId = importedBook.id) }
|
.map { it.copy(bookId = importedBook.id) }
|
||||||
.forEach { readProgressRepository.save(it) }
|
.forEach { readProgressRepository.save(it) }
|
||||||
|
|
||||||
// replace upgraded book by imported book in read lists
|
// replace upgraded book by imported book in read lists
|
||||||
readListRepository.findAllContainingBookId(upgradedBookId, filterOnLibraryIds = null)
|
readListRepository.findAllContainingBookId(upgradedBook.id, filterOnLibraryIds = null)
|
||||||
.forEach { rl ->
|
.forEach { rl ->
|
||||||
readListRepository.update(
|
readListRepository.update(
|
||||||
rl.copy(
|
rl.copy(
|
||||||
bookIds = rl.bookIds.values.map { if (it == upgradedBookId) importedBook.id else it }.toIndexedMap()
|
bookIds = rl.bookIds.values.map { if (it == upgradedBook.id) importedBook.id else it }.toIndexedMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -141,11 +147,18 @@ class BookImporter(
|
||||||
logger.info { "Deleted existing file: $upgradedBookPath" }
|
logger.info { "Deleted existing file: $upgradedBookPath" }
|
||||||
|
|
||||||
// delete upgraded book
|
// delete upgraded book
|
||||||
bookLifecycle.deleteOne(upgradedBookId)
|
bookLifecycle.deleteOne(upgradedBook)
|
||||||
}
|
}
|
||||||
|
|
||||||
seriesLifecycle.sortBooks(series)
|
seriesLifecycle.sortBooks(series)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.BookImported(importedBook, sourceFile.toUri().toURL(), success = true))
|
||||||
|
|
||||||
return importedBook
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.gotson.komga.application.events.EventPublisher
|
||||||
import org.gotson.komga.domain.model.Book
|
import org.gotson.komga.domain.model.Book
|
||||||
import org.gotson.komga.domain.model.BookPageContent
|
import org.gotson.komga.domain.model.BookPageContent
|
||||||
import org.gotson.komga.domain.model.BookWithMedia
|
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.ImageConversionException
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
import org.gotson.komga.domain.model.KomgaUser
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
|
|
@ -34,7 +36,8 @@ class BookLifecycle(
|
||||||
private val thumbnailBookRepository: ThumbnailBookRepository,
|
private val thumbnailBookRepository: ThumbnailBookRepository,
|
||||||
private val readListRepository: ReadListRepository,
|
private val readListRepository: ReadListRepository,
|
||||||
private val bookAnalyzer: BookAnalyzer,
|
private val bookAnalyzer: BookAnalyzer,
|
||||||
private val imageConverter: ImageConverter
|
private val imageConverter: ImageConverter,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun analyzeAndPersist(book: Book): Boolean {
|
fun analyzeAndPersist(book: Book): Boolean {
|
||||||
|
|
@ -49,6 +52,9 @@ class BookLifecycle(
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaRepository.update(media)
|
mediaRepository.update(media)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.BookUpdated(book))
|
||||||
|
|
||||||
return media.status == Media.Status.READY
|
return media.status == Media.Status.READY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,6 +85,8 @@ class BookLifecycle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail))
|
||||||
|
|
||||||
if (thumbnail.selected)
|
if (thumbnail.selected)
|
||||||
thumbnailBookRepository.markSelected(thumbnail)
|
thumbnailBookRepository.markSelected(thumbnail)
|
||||||
else
|
else
|
||||||
|
|
@ -187,21 +195,24 @@ class BookLifecycle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteOne(bookId: String) {
|
fun deleteOne(book: Book) {
|
||||||
logger.info { "Delete book id: $bookId" }
|
logger.info { "Delete book id: ${book.id}" }
|
||||||
|
|
||||||
readProgressRepository.deleteByBookId(bookId)
|
readProgressRepository.deleteByBookId(book.id)
|
||||||
readListRepository.removeBookFromAll(bookId)
|
readListRepository.removeBookFromAll(book.id)
|
||||||
|
|
||||||
mediaRepository.delete(bookId)
|
mediaRepository.delete(book.id)
|
||||||
thumbnailBookRepository.deleteByBookId(bookId)
|
thumbnailBookRepository.deleteByBookId(book.id)
|
||||||
bookMetadataRepository.delete(bookId)
|
bookMetadataRepository.delete(book.id)
|
||||||
|
|
||||||
bookRepository.delete(bookId)
|
bookRepository.delete(book.id)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.BookDeleted(book))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMany(bookIds: Collection<String>) {
|
fun deleteMany(books: Collection<Book>) {
|
||||||
logger.info { "Delete all books: $bookIds" }
|
val bookIds = books.map { it.id }
|
||||||
|
logger.info { "Delete book ids: $bookIds" }
|
||||||
|
|
||||||
readProgressRepository.deleteByBookIds(bookIds)
|
readProgressRepository.deleteByBookIds(bookIds)
|
||||||
readListRepository.removeBooksFromAll(bookIds)
|
readListRepository.removeBooksFromAll(bookIds)
|
||||||
|
|
@ -211,22 +222,31 @@ class BookLifecycle(
|
||||||
bookMetadataRepository.delete(bookIds)
|
bookMetadataRepository.delete(bookIds)
|
||||||
|
|
||||||
bookRepository.delete(bookIds)
|
bookRepository.delete(bookIds)
|
||||||
|
|
||||||
|
books.forEach { eventPublisher.publishEvent(DomainEvent.BookDeleted(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
|
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
|
||||||
val pages = mediaRepository.getPagesSize(book.id)
|
val pages = mediaRepository.getPagesSize(book.id)
|
||||||
require(page in 1..pages) { "Page argument ($page) must be within 1 and book page count ($pages)" }
|
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) {
|
fun markReadProgressCompleted(bookId: String, user: KomgaUser) {
|
||||||
val media = mediaRepository.findById(bookId)
|
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) {
|
fun deleteReadProgress(book: Book, user: KomgaUser) {
|
||||||
readProgressRepository.delete(bookId, user.id)
|
readProgressRepository.findByBookIdAndUserIdOrNull(book.id, user.id)?.let { progress ->
|
||||||
|
readProgressRepository.delete(book.id, user.id)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(progress))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,14 @@ class LibraryContentLifecycle(
|
||||||
// delete series that don't exist anymore
|
// delete series that don't exist anymore
|
||||||
if (scannedSeries.isEmpty()) {
|
if (scannedSeries.isEmpty()) {
|
||||||
logger.info { "Scan returned no series, deleting all existing series" }
|
logger.info { "Scan returned no series, deleting all existing series" }
|
||||||
val seriesIds = seriesRepository.findAllByLibraryId(library.id).map { it.id }
|
val series = seriesRepository.findAllByLibraryId(library.id)
|
||||||
seriesLifecycle.deleteMany(seriesIds)
|
seriesLifecycle.deleteMany(series)
|
||||||
} else {
|
} else {
|
||||||
scannedSeries.keys.map { it.url }.let { urls ->
|
scannedSeries.keys.map { it.url }.let { urls ->
|
||||||
val series = seriesRepository.findAllByLibraryIdAndUrlNotIn(library.id, urls)
|
val series = seriesRepository.findAllByLibraryIdAndUrlNotIn(library.id, urls)
|
||||||
if (series.isNotEmpty()) {
|
if (series.isNotEmpty()) {
|
||||||
logger.info { "Deleting series not on disk anymore: $series" }
|
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) }
|
.filterNot { existingBook -> newBooksUrls.contains(existingBook.url) }
|
||||||
.let { books ->
|
.let { books ->
|
||||||
logger.info { "Deleting books not on disk anymore: $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) }
|
books.map { it.seriesId }.distinct().forEach { taskReceiver.refreshSeriesMetadata(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.gotson.komga.application.events.EventPublisher
|
||||||
import org.gotson.komga.application.tasks.TaskReceiver
|
import org.gotson.komga.application.tasks.TaskReceiver
|
||||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
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.DuplicateNameException
|
||||||
import org.gotson.komga.domain.model.Library
|
import org.gotson.komga.domain.model.Library
|
||||||
import org.gotson.komga.domain.model.PathContainedInPath
|
import org.gotson.komga.domain.model.PathContainedInPath
|
||||||
|
|
@ -21,7 +23,8 @@ class LibraryLifecycle(
|
||||||
private val seriesLifecycle: SeriesLifecycle,
|
private val seriesLifecycle: SeriesLifecycle,
|
||||||
private val seriesRepository: SeriesRepository,
|
private val seriesRepository: SeriesRepository,
|
||||||
private val sidecarRepository: SidecarRepository,
|
private val sidecarRepository: SidecarRepository,
|
||||||
private val taskReceiver: TaskReceiver
|
private val taskReceiver: TaskReceiver,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
|
|
@ -39,6 +42,8 @@ class LibraryLifecycle(
|
||||||
libraryRepository.insert(library)
|
libraryRepository.insert(library)
|
||||||
taskReceiver.scanLibrary(library.id)
|
taskReceiver.scanLibrary(library.id)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.LibraryAdded(library))
|
||||||
|
|
||||||
return libraryRepository.findById(library.id)
|
return libraryRepository.findById(library.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +55,8 @@ class LibraryLifecycle(
|
||||||
|
|
||||||
libraryRepository.update(toUpdate)
|
libraryRepository.update(toUpdate)
|
||||||
taskReceiver.scanLibrary(toUpdate.id)
|
taskReceiver.scanLibrary(toUpdate.id)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.LibraryUpdated(toUpdate))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkLibraryValidity(library: Library, existing: Collection<Library>) {
|
private fun checkLibraryValidity(library: Library, existing: Collection<Library>) {
|
||||||
|
|
@ -73,10 +80,12 @@ class LibraryLifecycle(
|
||||||
fun deleteLibrary(library: Library) {
|
fun deleteLibrary(library: Library) {
|
||||||
logger.info { "Deleting library: $library" }
|
logger.info { "Deleting library: $library" }
|
||||||
|
|
||||||
val seriesIds = seriesRepository.findAllByLibraryId(library.id).map { it.id }
|
val series = seriesRepository.findAllByLibraryId(library.id)
|
||||||
seriesLifecycle.deleteMany(seriesIds)
|
seriesLifecycle.deleteMany(series)
|
||||||
sidecarRepository.deleteByLibraryId(library.id)
|
sidecarRepository.deleteByLibraryId(library.id)
|
||||||
|
|
||||||
libraryRepository.delete(library.id)
|
libraryRepository.delete(library.id)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.LibraryDeleted(library))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.gotson.komga.application.events.EventPublisher
|
||||||
import org.gotson.komga.domain.model.Book
|
import org.gotson.komga.domain.model.Book
|
||||||
import org.gotson.komga.domain.model.BookMetadataPatch
|
import org.gotson.komga.domain.model.BookMetadataPatch
|
||||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||||
import org.gotson.komga.domain.model.BookWithMedia
|
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.ReadList
|
||||||
import org.gotson.komga.domain.model.Series
|
import org.gotson.komga.domain.model.Series
|
||||||
import org.gotson.komga.domain.model.SeriesCollection
|
import org.gotson.komga.domain.model.SeriesCollection
|
||||||
|
|
@ -42,6 +44,7 @@ class MetadataLifecycle(
|
||||||
private val collectionLifecycle: SeriesCollectionLifecycle,
|
private val collectionLifecycle: SeriesCollectionLifecycle,
|
||||||
private val readListRepository: ReadListRepository,
|
private val readListRepository: ReadListRepository,
|
||||||
private val readListLifecycle: ReadListLifecycle,
|
private val readListLifecycle: ReadListLifecycle,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun refreshMetadata(book: Book, capabilities: List<BookMetadataPatchCapability>) {
|
fun refreshMetadata(book: Book, capabilities: List<BookMetadataPatchCapability>) {
|
||||||
|
|
@ -49,6 +52,7 @@ class MetadataLifecycle(
|
||||||
val media = mediaRepository.findById(book.id)
|
val media = mediaRepository.findById(book.id)
|
||||||
|
|
||||||
val library = libraryRepository.findById(book.libraryId)
|
val library = libraryRepository.findById(book.libraryId)
|
||||||
|
var changed = false
|
||||||
|
|
||||||
bookMetadataProviders.forEach { provider ->
|
bookMetadataProviders.forEach { provider ->
|
||||||
when {
|
when {
|
||||||
|
|
@ -70,6 +74,7 @@ class MetadataLifecycle(
|
||||||
(provider is IsbnBarcodeProvider && library.importBarcodeIsbn)
|
(provider is IsbnBarcodeProvider && library.importBarcodeIsbn)
|
||||||
) {
|
) {
|
||||||
handlePatchForBookMetadata(patch, book)
|
handlePatchForBookMetadata(patch, book)
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider is ComicInfoProvider && library.importComicInfoReadList) {
|
if (provider is ComicInfoProvider && library.importComicInfoReadList) {
|
||||||
|
|
@ -78,6 +83,8 @@ class MetadataLifecycle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changed) eventPublisher.publishEvent(DomainEvent.BookUpdated(book))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePatchForReadLists(
|
private fun handlePatchForReadLists(
|
||||||
|
|
@ -138,6 +145,7 @@ class MetadataLifecycle(
|
||||||
logger.info { "Refresh metadata for series: $series" }
|
logger.info { "Refresh metadata for series: $series" }
|
||||||
|
|
||||||
val library = libraryRepository.findById(series.libraryId)
|
val library = libraryRepository.findById(series.libraryId)
|
||||||
|
var changed = false
|
||||||
|
|
||||||
seriesMetadataProviders.forEach { provider ->
|
seriesMetadataProviders.forEach { provider ->
|
||||||
when {
|
when {
|
||||||
|
|
@ -153,6 +161,7 @@ class MetadataLifecycle(
|
||||||
(provider is EpubMetadataProvider && library.importEpubSeries)
|
(provider is EpubMetadataProvider && library.importEpubSeries)
|
||||||
) {
|
) {
|
||||||
handlePatchForSeriesMetadata(patches, series)
|
handlePatchForSeriesMetadata(patches, series)
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider is ComicInfoProvider && library.importComicInfoCollection) {
|
if (provider is ComicInfoProvider && library.importComicInfoCollection) {
|
||||||
|
|
@ -161,6 +170,8 @@ class MetadataLifecycle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changed) eventPublisher.publishEvent(DomainEvent.SeriesUpdated(series))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePatchForCollections(
|
private fun handlePatchForCollections(
|
||||||
|
|
@ -226,6 +237,8 @@ class MetadataLifecycle(
|
||||||
val aggregation = metadataAggregator.aggregate(metadatas).copy(seriesId = series.id)
|
val aggregation = metadataAggregator.aggregate(metadatas).copy(seriesId = series.id)
|
||||||
|
|
||||||
bookMetadataAggregationRepository.update(aggregation)
|
bookMetadataAggregationRepository.update(aggregation)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.SeriesUpdated(series))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T, R : Any> Iterable<T>.mostFrequent(transform: (T) -> R?): R? {
|
private fun <T, R : Any> Iterable<T>.mostFrequent(transform: (T) -> R?): R? {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import mu.KotlinLogging
|
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.DuplicateNameException
|
||||||
import org.gotson.komga.domain.model.ReadList
|
import org.gotson.komga.domain.model.ReadList
|
||||||
import org.gotson.komga.domain.model.ReadListRequestResult
|
import org.gotson.komga.domain.model.ReadListRequestResult
|
||||||
|
|
@ -18,6 +20,7 @@ class ReadListLifecycle(
|
||||||
private val mosaicGenerator: MosaicGenerator,
|
private val mosaicGenerator: MosaicGenerator,
|
||||||
private val readListMatcher: ReadListMatcher,
|
private val readListMatcher: ReadListMatcher,
|
||||||
private val readListProvider: ReadListProvider,
|
private val readListProvider: ReadListProvider,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
|
|
@ -31,6 +34,8 @@ class ReadListLifecycle(
|
||||||
|
|
||||||
readListRepository.insert(readList)
|
readListRepository.insert(readList)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ReadListAdded(readList))
|
||||||
|
|
||||||
return readListRepository.findByIdOrNull(readList.id)!!
|
return readListRepository.findByIdOrNull(readList.id)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,10 +48,14 @@ class ReadListLifecycle(
|
||||||
throw DuplicateNameException("Read list name already exists")
|
throw DuplicateNameException("Read list name already exists")
|
||||||
|
|
||||||
readListRepository.update(toUpdate)
|
readListRepository.update(toUpdate)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ReadListUpdated(toUpdate))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteReadList(readListId: String) {
|
fun deleteReadList(readList: ReadList) {
|
||||||
readListRepository.delete(readListId)
|
readListRepository.delete(readList.id)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ReadListDeleted(readList))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnailBytes(readList: ReadList): ByteArray {
|
fun getThumbnailBytes(readList: ReadList): ByteArray {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import mu.KotlinLogging
|
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.DuplicateNameException
|
||||||
import org.gotson.komga.domain.model.SeriesCollection
|
import org.gotson.komga.domain.model.SeriesCollection
|
||||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||||
|
|
@ -13,7 +15,8 @@ private val logger = KotlinLogging.logger {}
|
||||||
class SeriesCollectionLifecycle(
|
class SeriesCollectionLifecycle(
|
||||||
private val collectionRepository: SeriesCollectionRepository,
|
private val collectionRepository: SeriesCollectionRepository,
|
||||||
private val seriesLifecycle: SeriesLifecycle,
|
private val seriesLifecycle: SeriesLifecycle,
|
||||||
private val mosaicGenerator: MosaicGenerator
|
private val mosaicGenerator: MosaicGenerator,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
|
|
@ -27,6 +30,8 @@ class SeriesCollectionLifecycle(
|
||||||
|
|
||||||
collectionRepository.insert(collection)
|
collectionRepository.insert(collection)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.CollectionAdded(collection))
|
||||||
|
|
||||||
return collectionRepository.findByIdOrNull(collection.id)!!
|
return collectionRepository.findByIdOrNull(collection.id)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,10 +45,13 @@ class SeriesCollectionLifecycle(
|
||||||
throw DuplicateNameException("Collection name already exists")
|
throw DuplicateNameException("Collection name already exists")
|
||||||
|
|
||||||
collectionRepository.update(toUpdate)
|
collectionRepository.update(toUpdate)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.CollectionUpdated(toUpdate))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCollection(collectionId: String) {
|
fun deleteCollection(collection: SeriesCollection) {
|
||||||
collectionRepository.delete(collectionId)
|
collectionRepository.delete(collection.id)
|
||||||
|
eventPublisher.publishEvent(DomainEvent.CollectionDeleted(collection))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnailBytes(collection: SeriesCollection): ByteArray {
|
fun getThumbnailBytes(collection: SeriesCollection): ByteArray {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package org.gotson.komga.domain.service
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.gotson.komga.application.events.EventPublisher
|
||||||
import org.gotson.komga.application.tasks.TaskReceiver
|
import org.gotson.komga.application.tasks.TaskReceiver
|
||||||
import org.gotson.komga.domain.model.Book
|
import org.gotson.komga.domain.model.Book
|
||||||
import org.gotson.komga.domain.model.BookMetadata
|
import org.gotson.komga.domain.model.BookMetadata
|
||||||
import org.gotson.komga.domain.model.BookMetadataAggregation
|
import org.gotson.komga.domain.model.BookMetadataAggregation
|
||||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
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.KomgaUser
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
import org.gotson.komga.domain.model.ReadProgress
|
import org.gotson.komga.domain.model.ReadProgress
|
||||||
|
|
@ -43,7 +45,8 @@ class SeriesLifecycle(
|
||||||
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
|
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
|
||||||
private val collectionRepository: SeriesCollectionRepository,
|
private val collectionRepository: SeriesCollectionRepository,
|
||||||
private val readProgressRepository: ReadProgressRepository,
|
private val readProgressRepository: ReadProgressRepository,
|
||||||
private val taskReceiver: TaskReceiver
|
private val taskReceiver: TaskReceiver,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun sortBooks(series: Series) {
|
fun sortBooks(series: Series) {
|
||||||
|
|
@ -90,16 +93,15 @@ class SeriesLifecycle(
|
||||||
booksToAdd.forEach {
|
booksToAdd.forEach {
|
||||||
check(it.libraryId == series.libraryId) { "Cannot add book to series if they don't share the same libraryId" }
|
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(
|
bookRepository.insert(toAdd)
|
||||||
booksToAdd.map { it.copy(seriesId = series.id) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// create associated media
|
// create associated media
|
||||||
mediaRepository.insert(booksToAdd.map { Media(bookId = it.id) })
|
mediaRepository.insert(toAdd.map { Media(bookId = it.id) })
|
||||||
|
|
||||||
// create associated metadata
|
// create associated metadata
|
||||||
booksToAdd.map {
|
toAdd.map {
|
||||||
BookMetadata(
|
BookMetadata(
|
||||||
title = it.name,
|
title = it.name,
|
||||||
number = it.number.toString(),
|
number = it.number.toString(),
|
||||||
|
|
@ -107,6 +109,8 @@ class SeriesLifecycle(
|
||||||
bookId = it.id
|
bookId = it.id
|
||||||
)
|
)
|
||||||
}.let { bookMetadataRepository.insert(it) }
|
}.let { bookMetadataRepository.insert(it) }
|
||||||
|
|
||||||
|
toAdd.forEach { eventPublisher.publishEvent(DomainEvent.BookAdded(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createSeries(series: Series): Series {
|
fun createSeries(series: Series): Series {
|
||||||
|
|
@ -124,28 +128,17 @@ class SeriesLifecycle(
|
||||||
BookMetadataAggregation(seriesId = series.id)
|
BookMetadataAggregation(seriesId = series.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.SeriesAdded(series))
|
||||||
|
|
||||||
return seriesRepository.findByIdOrNull(series.id)!!
|
return seriesRepository.findByIdOrNull(series.id)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteOne(seriesId: String) {
|
fun deleteMany(series: Collection<Series>) {
|
||||||
logger.info { "Delete series id: $seriesId" }
|
val seriesIds = series.map { it.id }
|
||||||
|
|
||||||
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<String>) {
|
|
||||||
logger.info { "Delete series ids: $seriesIds" }
|
logger.info { "Delete series ids: $seriesIds" }
|
||||||
|
|
||||||
val bookIds = bookRepository.findAllIdsBySeriesIds(seriesIds)
|
val books = bookRepository.findAllBySeriesIds(seriesIds)
|
||||||
bookLifecycle.deleteMany(bookIds)
|
bookLifecycle.deleteMany(books)
|
||||||
|
|
||||||
collectionRepository.removeSeriesFromAll(seriesIds)
|
collectionRepository.removeSeriesFromAll(seriesIds)
|
||||||
thumbnailsSeriesRepository.deleteBySeriesIds(seriesIds)
|
thumbnailsSeriesRepository.deleteBySeriesIds(seriesIds)
|
||||||
|
|
@ -153,6 +146,8 @@ class SeriesLifecycle(
|
||||||
bookMetadataAggregationRepository.delete(seriesIds)
|
bookMetadataAggregationRepository.delete(seriesIds)
|
||||||
|
|
||||||
seriesRepository.delete(seriesIds)
|
seriesRepository.delete(seriesIds)
|
||||||
|
|
||||||
|
series.forEach { eventPublisher.publishEvent(DomainEvent.SeriesDeleted(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markReadProgressCompleted(seriesId: String, user: KomgaUser) {
|
fun markReadProgressCompleted(seriesId: String, user: KomgaUser) {
|
||||||
|
|
@ -160,10 +155,15 @@ class SeriesLifecycle(
|
||||||
.map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) }
|
.map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) }
|
||||||
|
|
||||||
readProgressRepository.save(progresses)
|
readProgressRepository.save(progresses)
|
||||||
|
progresses.forEach { eventPublisher.publishEvent(DomainEvent.ReadProgressChanged(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteReadProgress(seriesId: String, user: KomgaUser) {
|
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? {
|
fun getThumbnail(seriesId: String): ThumbnailSeries? {
|
||||||
|
|
@ -197,6 +197,8 @@ class SeriesLifecycle(
|
||||||
}
|
}
|
||||||
thumbnailsSeriesRepository.insert(thumbnail)
|
thumbnailsSeriesRepository.insert(thumbnail)
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail))
|
||||||
|
|
||||||
if (thumbnail.selected)
|
if (thumbnail.selected)
|
||||||
thumbnailsSeriesRepository.markSelected(thumbnail)
|
thumbnailsSeriesRepository.markSelected(thumbnail)
|
||||||
}
|
}
|
||||||
|
|
@ -224,5 +226,6 @@ class SeriesLifecycle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
|
private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.QueueConfiguration
|
||||||
import org.apache.activemq.artemis.api.core.RoutingType
|
import org.apache.activemq.artemis.api.core.RoutingType
|
||||||
import org.apache.activemq.artemis.core.settings.impl.AddressSettings
|
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.boot.autoconfigure.jms.artemis.ArtemisConfigurationCustomizer
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
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
|
import org.apache.activemq.artemis.core.config.Configuration as ArtemisConfiguration
|
||||||
|
|
||||||
const val QUEUE_UNIQUE_ID = "unique_id"
|
const val QUEUE_UNIQUE_ID = "unique_id"
|
||||||
const val QUEUE_TYPE = "type"
|
const val QUEUE_TYPE = "type"
|
||||||
|
|
||||||
const val QUEUE_TASKS = "tasks.background"
|
const val QUEUE_TASKS = "tasks.background"
|
||||||
const val QUEUE_TASKS_TYPE = "task"
|
const val QUEUE_TASKS_TYPE = "task"
|
||||||
const val QUEUE_TASKS_SELECTOR = "$QUEUE_TYPE = '$QUEUE_TASKS_TYPE'"
|
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
|
@Configuration
|
||||||
class ArtemisConfig : ArtemisConfigurationCustomizer {
|
class ArtemisConfig : ArtemisConfigurationCustomizer {
|
||||||
override fun customize(configuration: ArtemisConfiguration?) {
|
override fun customize(configuration: ArtemisConfiguration?) {
|
||||||
|
|
@ -32,6 +43,21 @@ class ArtemisConfig : ArtemisConfigurationCustomizer {
|
||||||
.setLastValueKey(QUEUE_UNIQUE_ID)
|
.setLastValueKey(QUEUE_UNIQUE_ID)
|
||||||
.setRoutingType(RoutingType.ANYCAST)
|
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,12 @@ class BookDao(
|
||||||
.fetchInto(b)
|
.fetchInto(b)
|
||||||
.map { it.toDomain() }
|
.map { it.toDomain() }
|
||||||
|
|
||||||
|
override fun findAllBySeriesIds(seriesIds: Collection<String>): Collection<Book> =
|
||||||
|
dsl.selectFrom(b)
|
||||||
|
.where(b.SERIES_ID.`in`(seriesIds))
|
||||||
|
.fetchInto(b)
|
||||||
|
.map { it.toDomain() }
|
||||||
|
|
||||||
override fun findAll(): Collection<Book> =
|
override fun findAll(): Collection<Book> =
|
||||||
dsl.select(*b.fields())
|
dsl.select(*b.fields())
|
||||||
.from(b)
|
.from(b)
|
||||||
|
|
@ -106,6 +112,12 @@ class BookDao(
|
||||||
.where(b.ID.eq(bookId))
|
.where(b.ID.eq(bookId))
|
||||||
.fetchOne(b.LIBRARY_ID)
|
.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? =
|
override fun findFirstIdInSeriesOrNull(seriesId: String): String? =
|
||||||
dsl.select(b.ID)
|
dsl.select(b.ID)
|
||||||
.from(b)
|
.from(b)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ class ReadProgressDao(
|
||||||
.fetchInto(r)
|
.fetchInto(r)
|
||||||
.map { it.toDomain() }
|
.map { it.toDomain() }
|
||||||
|
|
||||||
|
override fun findAllByBookIdsAndUserId(bookIds: Collection<String>, userId: String): Collection<ReadProgress> =
|
||||||
|
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) {
|
override fun save(readProgress: ReadProgress) {
|
||||||
dsl.transaction { config ->
|
dsl.transaction { config ->
|
||||||
config.dsl().saveQuery(readProgress).execute()
|
config.dsl().saveQuery(readProgress).execute()
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ class SecurityConfiguration(
|
||||||
// all other endpoints are restricted to authenticated users
|
// all other endpoints are restricted to authenticated users
|
||||||
.antMatchers(
|
.antMatchers(
|
||||||
"/api/**",
|
"/api/**",
|
||||||
"/opds/**"
|
"/opds/**",
|
||||||
|
"/sse/**"
|
||||||
).hasRole(ROLE_USER)
|
).hasRole(ROLE_USER)
|
||||||
|
|
||||||
.and()
|
.and()
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.apache.commons.io.IOUtils
|
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.HIGHEST_PRIORITY
|
||||||
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
||||||
import org.gotson.komga.application.tasks.TaskReceiver
|
import org.gotson.komga.application.tasks.TaskReceiver
|
||||||
import org.gotson.komga.domain.model.Author
|
import org.gotson.komga.domain.model.Author
|
||||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||||
|
import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.ImageConversionException
|
import org.gotson.komga.domain.model.ImageConversionException
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||||
|
|
@ -86,6 +88,7 @@ class BookController(
|
||||||
private val bookDtoRepository: BookDtoRepository,
|
private val bookDtoRepository: BookDtoRepository,
|
||||||
private val readListRepository: ReadListRepository,
|
private val readListRepository: ReadListRepository,
|
||||||
private val contentDetector: ContentDetector,
|
private val contentDetector: ContentDetector,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PageableAsQueryParam
|
@PageableAsQueryParam
|
||||||
|
|
@ -471,6 +474,8 @@ class BookController(
|
||||||
}
|
}
|
||||||
bookMetadataRepository.update(updated)
|
bookMetadataRepository.update(updated)
|
||||||
taskReceiver.aggregateSeriesMetadata(bookRepository.findByIdOrNull(bookId)!!.seriesId)
|
taskReceiver.aggregateSeriesMetadata(bookRepository.findByIdOrNull(bookId)!!.seriesId)
|
||||||
|
|
||||||
|
bookRepository.findByIdOrNull(bookId)?.let { eventPublisher.publishEvent(DomainEvent.BookUpdated(it)) }
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
@PatchMapping("api/v1/books/{bookId}/read-progress")
|
@PatchMapping("api/v1/books/{bookId}/read-progress")
|
||||||
|
|
@ -504,7 +509,7 @@ class BookController(
|
||||||
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
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)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ class ReadListController(
|
||||||
@PathVariable id: String
|
@PathVariable id: String
|
||||||
) {
|
) {
|
||||||
readListRepository.findByIdOrNull(id)?.let {
|
readListRepository.findByIdOrNull(id)?.let {
|
||||||
readListLifecycle.deleteReadList(it.id)
|
readListLifecycle.deleteReadList(it)
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ class SeriesCollectionController(
|
||||||
@PathVariable id: String
|
@PathVariable id: String
|
||||||
) {
|
) {
|
||||||
collectionRepository.findByIdOrNull(id)?.let {
|
collectionRepository.findByIdOrNull(id)?.let {
|
||||||
collectionLifecycle.deleteCollection(it.id)
|
collectionLifecycle.deleteCollection(it)
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ import mu.KotlinLogging
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||||
import org.apache.commons.io.IOUtils
|
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.HIGH_PRIORITY
|
||||||
import org.gotson.komga.application.tasks.TaskReceiver
|
import org.gotson.komga.application.tasks.TaskReceiver
|
||||||
import org.gotson.komga.domain.model.Author
|
import org.gotson.komga.domain.model.Author
|
||||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||||
|
import org.gotson.komga.domain.model.DomainEvent
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||||
|
|
@ -86,6 +88,7 @@ class SeriesController(
|
||||||
private val bookDtoRepository: BookDtoRepository,
|
private val bookDtoRepository: BookDtoRepository,
|
||||||
private val collectionRepository: SeriesCollectionRepository,
|
private val collectionRepository: SeriesCollectionRepository,
|
||||||
private val readProgressDtoRepository: ReadProgressDtoRepository,
|
private val readProgressDtoRepository: ReadProgressDtoRepository,
|
||||||
|
private val eventPublisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PageableAsQueryParam
|
@PageableAsQueryParam
|
||||||
|
|
@ -353,6 +356,8 @@ class SeriesController(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
seriesMetadataRepository.update(updated)
|
seriesMetadataRepository.update(updated)
|
||||||
|
|
||||||
|
seriesRepository.findByIdOrNull(seriesId)?.let { eventPublisher.publishEvent(DomainEvent.SeriesUpdated(it)) }
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
@PostMapping("{seriesId}/read-progress")
|
@PostMapping("{seriesId}/read-progress")
|
||||||
|
|
|
||||||
|
|
@ -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<SseEmitter, KomgaUser>())
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class BookSseDto(
|
||||||
|
val bookId: String,
|
||||||
|
val seriesId: String,
|
||||||
|
val libraryId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class CollectionSseDto(
|
||||||
|
val collectionId: String,
|
||||||
|
val seriesIds: List<String>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class LibrarySseDto(
|
||||||
|
val libraryId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class ReadListSseDto(
|
||||||
|
val readListId: String,
|
||||||
|
val bookIds: List<String>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class ReadProgressSseDto(
|
||||||
|
val bookId: String,
|
||||||
|
val userId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class SeriesSseDto(
|
||||||
|
val seriesId: String,
|
||||||
|
val libraryId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class TaskQueueSseDto(
|
||||||
|
val count: Int,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class ThumbnailBookSseDto(
|
||||||
|
val bookId: String,
|
||||||
|
val seriesId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.gotson.komga.interfaces.sse.dto
|
||||||
|
|
||||||
|
data class ThumbnailSeriesSseDto(
|
||||||
|
val seriesId: String,
|
||||||
|
)
|
||||||
|
|
@ -4,7 +4,7 @@ komga:
|
||||||
validity: 2592000 # 1 month
|
validity: 2592000 # 1 month
|
||||||
# libraries-scan-cron: "*/5 * * * * ?" #every 5 seconds
|
# libraries-scan-cron: "*/5 * * * * ?" #every 5 seconds
|
||||||
libraries-scan-cron: "-" #disable
|
libraries-scan-cron: "-" #disable
|
||||||
libraries-scan-startup: true
|
libraries-scan-startup: false
|
||||||
database:
|
database:
|
||||||
file: ":memory:"
|
file: ":memory:"
|
||||||
cors.allowed-origins:
|
cors.allowed-origins:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ logging:
|
||||||
file:
|
file:
|
||||||
name: \${user.home}/.komga/komga.log
|
name: \${user.home}/.komga/komga.log
|
||||||
level:
|
level:
|
||||||
org.apache.activemq.audit.message: WARN
|
org.apache.activemq.audit: WARN
|
||||||
|
|
||||||
komga:
|
komga:
|
||||||
libraries-scan-cron: "0 */15 * * * ?"
|
libraries-scan-cron: "0 */15 * * * ?"
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class BookImporterTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun `clear repository`() {
|
fun `clear repository`() {
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class BookLifecycleTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun `clear repository`() {
|
fun `clear repository`() {
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class ReadListMatcherTest(
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun `clear repository`() {
|
fun `clear repository`() {
|
||||||
readListRepository.deleteAll()
|
readListRepository.deleteAll()
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class SeriesLifecycleTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun `clear repository`() {
|
fun `clear repository`() {
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -88,7 +88,7 @@ class SeriesLifecycleTest(
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val book = bookRepository.findAllBySeriesId(createdSeries.id).first { it.name == "book 2" }
|
val book = bookRepository.findAllBySeriesId(createdSeries.id).first { it.name == "book 2" }
|
||||||
bookLifecycle.deleteOne(book.id)
|
bookLifecycle.deleteOne(book)
|
||||||
seriesLifecycle.sortBooks(createdSeries)
|
seriesLifecycle.sortBooks(createdSeries)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class BookDtoDaoTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun deleteBooks() {
|
fun deleteBooks() {
|
||||||
bookLifecycle.deleteMany(bookRepository.findAll().map { it.id })
|
bookLifecycle.deleteMany(bookRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class SeriesDtoDaoTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun deleteSeries() {
|
fun deleteSeries() {
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ class BookControllerTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun `clear repository`() {
|
fun `clear repository`() {
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class SeriesControllerTest(
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun `clear repository`() {
|
fun `clear repository`() {
|
||||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue