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:
Gauthier Roebroeck 2019-08-23 14:25:10 +08:00
parent 6c0d953ad6
commit 4639bc1281
9 changed files with 97 additions and 23 deletions

View file

@ -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>
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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)})" }
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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" }
}
}
}

View file

@ -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")
}
}
}

View file

@ -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