mirror of
https://github.com/gotson/komga.git
synced 2026-04-17 12:33:33 +02:00
added admin endpoint to regenerate missing thumbnails
fix admin endpoint to regenerate thumbnails which would block instead of returning. Added task management with single capacity, so subsequent calls will be dropped if a task is running more logs with book url for errors expose configuration for number of threads for the parsing executor add single capacity task executor for periodic scans, so subsequent scans will be skipped if one is already running
This commit is contained in:
parent
6c0d953ad6
commit
4639bc1281
9 changed files with 97 additions and 23 deletions
|
|
@ -14,4 +14,5 @@ interface BookRepository : JpaRepository<Book, Long> {
|
|||
fun findByUrl(url: URL): Book?
|
||||
fun findAllByMetadataStatus(status: Status): List<Book>
|
||||
fun findAllByMetadataStatusAndSerieId(status: Status, serieId: Long, pageable: Pageable): Page<Book>
|
||||
fun findAllByMetadataThumbnailIsNull(): List<Book>
|
||||
}
|
||||
|
|
@ -29,10 +29,10 @@ class BookManager(
|
|||
try {
|
||||
book.metadata = bookParser.parse(book)
|
||||
} catch (ex: UnsupportedMediaTypeException) {
|
||||
logger.info(ex) { "Unsupported media type: ${ex.mediaType}" }
|
||||
logger.info(ex) { "Unsupported media type: ${ex.mediaType}. Book: ${book.url}" }
|
||||
book.metadata = BookMetadata(status = Status.UNSUPPORTED, mediaType = ex.mediaType)
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Error while parsing" }
|
||||
logger.error(ex) { "Error while parsing. Book: ${book.url}" }
|
||||
book.metadata = BookMetadata(status = Status.ERROR)
|
||||
}
|
||||
bookRepository.save(book)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class BookParser(
|
|||
logger.info { "Regenerate thumbnail for book: ${book.url}" }
|
||||
|
||||
if (book.metadata.status != Status.READY) {
|
||||
logger.warn { "Book metadata is not ready, cannot generate thumbnail" }
|
||||
logger.warn { "Book metadata is not ready, cannot generate thumbnail. Book: ${book.url}" }
|
||||
throw MetadataNotReadyException()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package org.gotson.komga.domain.service
|
|||
|
||||
import mu.KotlinLogging
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.springframework.data.auditing.AuditingHandler
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
|
@ -95,14 +97,31 @@ class LibraryManager(
|
|||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Async("periodicScanTaskExecutor")
|
||||
fun scanAndParse(library: Library) {
|
||||
logger.info { "Starting periodic library scan" }
|
||||
scanRootFolder(library)
|
||||
|
||||
logger.info { "Starting periodic book parsing" }
|
||||
parseUnparsedBooks()
|
||||
}
|
||||
|
||||
@Async("regenerateThumbnailsTaskExecutor")
|
||||
fun regenerateAllThumbnails() {
|
||||
logger.info { "Regenerate thumbnail for all books" }
|
||||
val booksToProcess = bookRepository.findAll()
|
||||
generateThumbnails(bookRepository.findAll())
|
||||
}
|
||||
|
||||
@Async("regenerateThumbnailsTaskExecutor")
|
||||
fun regenerateMissingThumbnails() {
|
||||
logger.info { "Regenerate missing thumbnails" }
|
||||
generateThumbnails(bookRepository.findAllByMetadataThumbnailIsNull())
|
||||
}
|
||||
|
||||
private fun generateThumbnails(books: List<Book>) {
|
||||
var sumOfTasksTime = 0L
|
||||
measureTimeMillis {
|
||||
sumOfTasksTime = booksToProcess
|
||||
sumOfTasksTime = books
|
||||
.map { bookManager.regenerateThumbnailAndPersist(it) }
|
||||
.map {
|
||||
try {
|
||||
|
|
@ -113,7 +132,7 @@ class LibraryManager(
|
|||
}
|
||||
.sum()
|
||||
}.also {
|
||||
logger.info { "Generated ${booksToProcess.size} thumbnails in ${DurationFormatUtils.formatDurationHMS(it)} (virtual: ${DurationFormatUtils.formatDurationHMS(sumOfTasksTime)})" }
|
||||
logger.info { "Generated ${books.size} thumbnails in ${DurationFormatUtils.formatDurationHMS(it)} (virtual: ${DurationFormatUtils.formatDurationHMS(sumOfTasksTime)})" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package org.gotson.komga.infrastructure.async
|
||||
|
||||
import org.gotson.komga.infrastructure.configuration.KomgaProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
|
|
@ -8,11 +9,29 @@ import java.util.concurrent.Executor
|
|||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
class AsyncConfiguration {
|
||||
class AsyncConfiguration(
|
||||
private val komgaProperties: KomgaProperties
|
||||
) {
|
||||
|
||||
@Bean
|
||||
@Bean("parseBookTaskExecutor")
|
||||
fun parseBookTaskExecutor(): Executor =
|
||||
ThreadPoolTaskExecutor().apply {
|
||||
corePoolSize = 2
|
||||
corePoolSize = komgaProperties.threads.parse
|
||||
}
|
||||
|
||||
@Bean("periodicScanTaskExecutor")
|
||||
fun periodicScanTaskExecutor(): Executor =
|
||||
ThreadPoolTaskExecutor().apply {
|
||||
corePoolSize = 1
|
||||
maxPoolSize = 1
|
||||
setQueueCapacity(0)
|
||||
}
|
||||
|
||||
@Bean("regenerateThumbnailsTaskExecutor")
|
||||
fun regenerateThumbnailsTaskExecutor(): Executor =
|
||||
ThreadPoolTaskExecutor().apply {
|
||||
corePoolSize = 1
|
||||
maxPoolSize = 1
|
||||
setQueueCapacity(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,27 @@ package org.gotson.komga.infrastructure.configuration
|
|||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "komga")
|
||||
@Validated
|
||||
class KomgaProperties {
|
||||
var rootFolder: String = ""
|
||||
var rootFolderScanCron: String = ""
|
||||
var userPassword: String = "user"
|
||||
var adminPassword: String = "admin"
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
var userPassword: String = "user"
|
||||
|
||||
@NotBlank
|
||||
var adminPassword: String = "admin"
|
||||
|
||||
var threads = Threads()
|
||||
|
||||
class Threads {
|
||||
@Min(1)
|
||||
var parse: Int = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import org.springframework.context.annotation.Profile
|
|||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Controller
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -21,12 +22,11 @@ class RootScannerController(
|
|||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
@Scheduled(cron = "#{@komgaProperties.rootFolderScanCron ?: '-'}")
|
||||
@Synchronized
|
||||
fun scanRootFolder() {
|
||||
logger.info { "Starting periodic library scan" }
|
||||
libraryManager.scanRootFolder(Library("default", komgaProperties.rootFolder))
|
||||
|
||||
logger.info { "Starting periodic book parsing" }
|
||||
libraryManager.parseUnparsedBooks()
|
||||
try {
|
||||
libraryManager.scanAndParse(Library("default", komgaProperties.rootFolder))
|
||||
} catch (e: RejectedExecutionException) {
|
||||
logger.warn { "Another scan is already running, skipping" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package org.gotson.komga.interfaces.web
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.service.LibraryManager
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
|
@ -7,6 +8,10 @@ import org.springframework.web.bind.annotation.PostMapping
|
|||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/admin")
|
||||
|
|
@ -15,9 +20,23 @@ class AdminController(
|
|||
private val libraryManager: LibraryManager
|
||||
) {
|
||||
|
||||
@PostMapping("rpc/thumbnails/regenerateall")
|
||||
@PostMapping("rpc/thumbnails/regenerate/all")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun regenerateAllThumbnails() {
|
||||
libraryManager.regenerateAllThumbnails()
|
||||
try {
|
||||
libraryManager.regenerateAllThumbnails()
|
||||
} catch (e: RejectedExecutionException) {
|
||||
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Thumbnail regeneration task is already running")
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("rpc/thumbnails/regenerate/missing")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun regenerateMissingThumbnails() {
|
||||
try {
|
||||
libraryManager.regenerateMissingThumbnails()
|
||||
} catch (e: RejectedExecutionException) {
|
||||
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Thumbnail regeneration task is already running")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
#logging.level.org.springframework.security.web.FilterChainProxy: DEBUG
|
||||
komga:
|
||||
root-folder: D:\\files
|
||||
# root-folder-scan-cron: "*/10 * * * * ?"
|
||||
root-folder: D:\\files\\issues
|
||||
threads:
|
||||
parse: 1
|
||||
# root-folder-scan-cron: "*/5 * * * * ?"
|
||||
spring:
|
||||
profiles:
|
||||
include: flyway
|
||||
|
|
|
|||
Loading…
Reference in a new issue