rename book metadata to media, to avoid confusion later on when proper metadata is added

rename parser to analyzer, using the same vocabulary as plex
deprecation of komga.threads.parse configuration key in favor of komga.threads.analyzer
added created date and fileLastModified date to SeriesDto and BookDto
deprecation of ready_only parameter in /series/{id}/books in favor of media_status, this will enable better filtering in the web ui
This commit is contained in:
Gauthier Roebroeck 2019-12-30 14:40:00 +08:00
parent 38be19de33
commit ebad597f26
27 changed files with 175 additions and 124 deletions

View File

@ -58,7 +58,7 @@ In order to make Komga run, you need to specify some mandatory configuration key
You can also use some optional configuration keys:
- `KOMGA_LIBRARIES_SCAN_CRON` / `komga.libraries-scan-cron`: a [Spring cron expression](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html) for libraries periodic rescans. `0 0 * * * ?` will rescan every hour. `0 */15 * * * ?` will rescan every 15 minutes. Defaults to `0 */15 * * * ?` in `prod` profile.
- `KOMGA_THREADS_PARSE` / `komga.threads.parse`: the number of worker threads used for book parsing. Defaults to `2`. You can experiment to get better performance.
- `KOMGA_THREADS_ANALYZER` / `komga.threads.analyzer`: the number of worker threads used for analyzing books. Defaults to `2`. You can experiment to get better performance.
- `KOMGA_LIBRARIES_SCAN_DIRECTORY_EXCLUSIONS` / `komga.libraries-scan-directory-exclusions`: a list of patterns to exclude directories from the scan. If the full path contains any of the patterns, the directory will be ignored. If using the environment variable form use a comma-separated list.
- `KOMGA_FILESYSTEM_SCANNER_FORCE_DIRECTORY_MODIFIED_TIME` / `komga.filesystem-scanner-force-directory-modified-time`: if set to `true`, it will force the last modified time of a directory as the maximum from its own last modified time and the last modified time from all the books inside the directory. This should be used only if your filesystem does not update the last modified time of a directory when files inside it are modified (Google Drive for instance).
@ -86,7 +86,7 @@ Komga will generate:
On rescans, Komga will update Series and Books, add new ones, and remove the ones for which files don't exist anymore.
Then it will _parse_ each book, which consist of indexing pages (images in the archive), and generating a thumbnail.
Then it will _analyze_ each book, which consist of indexing pages (images in the archive), and generating a thumbnail.
## Security

View File

@ -51,7 +51,7 @@
</v-col>
<v-col cols="auto">
<v-icon class="mr-2 pb-1">mdi-book-open</v-icon>
<span class="body-2">{{ book.metadata.pagesCount }} pages</span>
<span class="body-2">{{ book.media.pagesCount }} pages</span>
</v-col>
</v-row>
@ -70,11 +70,11 @@
<v-row>
<v-col cols="2" md="1" lg="1" xl="1" class="body-2">FORMAT</v-col>
<v-col cols="10" class="body-2">
<template v-if="book.metadata.status === 'ERROR'">
<span class="error--text font-weight-bold">Book could not be parsed</span>
<template v-if="book.media.status === 'ERROR'">
<span class="error--text font-weight-bold">Book analysis failed</span>
</template>
<template v-else-if="book.metadata.status === 'UNSUPPORTED'">
<span class="error--text font-weight-bold">File type not supported: {{ book.metadata.mediaType }}</span>
<template v-else-if="book.media.status === 'UNSUPPORTED'">
<span class="error--text font-weight-bold">File type not supported: {{ book.media.mediaType }}</span>
</template>
<template v-else>{{ format.type }}</template>
</v-col>
@ -125,7 +125,7 @@ export default Vue.extend({
return `${this.baseURL}/api/v1/books/${this.bookId}/file`
},
format (): BookFormat {
return getBookFormatFromMediaType(this.book.metadata.mediaType)
return getBookFormatFromMediaType(this.book.media.mediaType)
},
barStyle (): any {
if (this.$vuetify.breakpoint.name === 'xs') {

View File

@ -226,7 +226,7 @@ export default Vue.extend({
if (this.sortActive != null) {
pageRequest.sort = [`${this.sortActive.key},${this.sortActive.order}`]
}
return this.$komgaSeries.getBooks(seriesId, pageRequest, false)
return this.$komgaSeries.getBooks(seriesId, pageRequest)
},
processPage (page: Page<BookDto>) {
if (this.totalElements === null) {

View File

@ -13,11 +13,11 @@
{{ format.type }}
</span>
<span v-if="book.metadata.status !== 'READY'"
<span v-if="book.media.status !== 'READY'"
class="white--text pa-1 px-2 subtitle-2"
:style="{background: statusColor, position: 'absolute', bottom: 0, width: `${width}px`}"
>
{{ book.metadata.status }}
{{ book.media.status }}
</span>
</v-img>
@ -32,8 +32,8 @@
<v-card-text class="px-2"
>
<div>{{ book.size }}</div>
<div v-if="book.metadata.pagesCount === 1">{{ book.metadata.pagesCount }} page</div>
<div v-else>{{ book.metadata.pagesCount }} pages</div>
<div v-if="book.media.pagesCount === 1">{{ book.media.pagesCount }} page</div>
<div v-else>{{ book.media.pagesCount }} pages</div>
</v-card-text>
</v-card>
@ -66,10 +66,10 @@ export default Vue.extend({
return `${this.baseURL}/api/v1/books/${this.book.id}/thumbnail`
},
format (): BookFormat {
return getBookFormatFromMediaType(this.book.metadata.mediaType)
return getBookFormatFromMediaType(this.book.media.mediaType)
},
statusColor (): string {
switch (this.book.metadata.status) {
switch (this.book.media.status) {
case 'ERROR':
return 'red'
case 'UNKOWN':

View File

@ -75,10 +75,10 @@ export default class KomgaSeriesService {
}
}
async getBooks (seriesId: number, pageRequest?: PageRequest, readyOnly: boolean = true): Promise<Page<BookDto>> {
async getBooks (seriesId: number, pageRequest?: PageRequest): Promise<Page<BookDto>> {
try {
return (await this.http.get(`${API_SERIES}/${seriesId}/books`, {
params: { ...pageRequest, ready_only: readyOnly },
params: { ...pageRequest },
paramsSerializer: params => qs.stringify(params, { indices: false })
})).data
} catch (e) {

View File

@ -7,10 +7,10 @@ interface BookDto {
lastModified: string,
sizeBytes: number,
size: string,
metadata: BookMetadataDto
media: MediaDto
}
interface BookMetadataDto {
interface MediaDto {
status: string,
mediaType: string,
pagesCount: number

View File

@ -75,7 +75,7 @@
<!-- Menu: number of pages -->
<v-row>
<v-col class="text-center title">
Page {{ currentPage }} of {{ book.metadata.pagesCount }}
Page {{ currentPage }} of {{ book.media.pagesCount }}
</v-col>
</v-row>
@ -114,7 +114,7 @@
<v-slider
v-model="goToPage"
class="align-center"
:max="book.metadata.pagesCount"
:max="book.media.pagesCount"
min="1"
hide-details
@change="goTo"
@ -248,10 +248,10 @@ export default Vue.extend({
return this.currentPage > 1
},
canNext (): boolean {
return this.currentPage < this.book.metadata.pagesCount
return this.currentPage < this.book.media.pagesCount
},
progress (): number {
return this.currentPage / this.book.metadata.pagesCount * 100
return this.currentPage / this.book.media.pagesCount * 100
},
maxHeight (): number | undefined {
return this.fitHeight ? this.$vuetify.breakpoint.height - 7 : undefined
@ -285,7 +285,7 @@ export default Vue.extend({
async setup (bookId: number, page: number) {
this.book = await this.$komgaBooks.getBook(bookId)
this.pages = await this.$komgaBooks.getBookPages(bookId)
if (page >= 1 && page <= this.book.metadata.pagesCount) {
if (page >= 1 && page <= this.book.media.pagesCount) {
this.currentPage = page
this.slickOptions.initialSlide = page - 1
} else {
@ -322,7 +322,7 @@ export default Vue.extend({
this.goTo(this.goToPage)
},
goToLast () {
this.goToPage = this.book.metadata.pagesCount
this.goToPage = this.book.media.pagesCount
this.goTo(this.goToPage)
},
updateRoute () {

View File

@ -51,8 +51,8 @@ class Book(
lateinit var series: Series
@OneToOne(optional = false, orphanRemoval = true, cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "book_metadata_id", nullable = false)
var metadata: BookMetadata = BookMetadata()
@JoinColumn(name = "media_id", nullable = false)
var media: Media = Media()
@Column(name = "number", nullable = false, columnDefinition = "REAL")
var number: Float = 0F

View File

@ -1,7 +1,7 @@
package org.gotson.komga.domain.model
class MetadataNotReadyException : Exception()
class MediaNotReadyException : Exception()
class UnsupportedMediaTypeException(message: String, val mediaType: String) : Exception(message)
class DirectoryNotFoundException(message: String) : Exception(message)
class DuplicateNameException(message: String) : Exception(message)
class PathContainedInPath(message: String) : Exception(message)
class PathContainedInPath(message: String) : Exception(message)

View File

@ -22,10 +22,10 @@ import javax.persistence.Table
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
@Entity
@Table(name = "book_metadata")
@Table(name = "media")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.bookmetadata")
class BookMetadata(
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.media")
class Media(
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: Status = Status.UNKNOWN,
@ -45,9 +45,9 @@ class BookMetadata(
val id: Long = 0
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "book_metadata_page", joinColumns = [JoinColumn(name = "book_metadata_id")])
@CollectionTable(name = "media_page", joinColumns = [JoinColumn(name = "media_id")])
@OrderColumn(name = "number")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.bookmetadata.collection.pages")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.media.collection.pages")
private var _pages: MutableList<BookPage> = mutableListOf()
var pages: List<BookPage>

View File

@ -1,8 +1,8 @@
package org.gotson.komga.domain.persistence
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.Library
import org.gotson.komga.domain.model.Media
import org.hibernate.annotations.QueryHints.CACHEABLE
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
@ -22,12 +22,12 @@ interface BookRepository : JpaRepository<Book, Long>, JpaSpecificationExecutor<B
fun findAllBySeriesId(seriesId: Long, pageable: Pageable): Page<Book>
@QueryHints(QueryHint(name = CACHEABLE, value = "true"))
fun findAllByMetadataStatusAndSeriesId(status: BookMetadata.Status, seriesId: Long, pageable: Pageable): Page<Book>
fun findAllByMediaStatusInAndSeriesId(status: Collection<Media.Status>, seriesId: Long, pageable: Pageable): Page<Book>
@QueryHints(QueryHint(name = CACHEABLE, value = "true"))
fun findBySeriesLibraryIn(seriesLibrary: Collection<Library>, pageable: Pageable): Page<Book>
fun findByUrl(url: URL): Book?
fun findAllByMetadataStatus(status: BookMetadata.Status): List<Book>
fun findAllByMetadataThumbnailIsNull(): List<Book>
fun findAllByMediaStatus(status: Media.Status): List<Book>
fun findAllByMediaThumbnailIsNull(): List<Book>
}

View File

@ -1,8 +1,8 @@
package org.gotson.komga.domain.persistence
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.Media
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface BookMetadataRepository : JpaRepository<BookMetadata, Long>
interface MediaRepository : JpaRepository<Media, Long>

View File

@ -20,7 +20,7 @@ class AsyncOrchestrator(
) {
@Async("periodicScanTaskExecutor")
fun scanAndParse() {
fun scanAndAnalyze() {
logger.info { "Starting periodic libraries scan" }
val libraries = libraryRepository.findAll()
@ -32,7 +32,7 @@ class AsyncOrchestrator(
}
logger.info { "Starting periodic book parsing" }
libraryScanner.parseUnparsedBooks()
libraryScanner.analyzeUnknownBooks()
}
}
@ -45,7 +45,7 @@ class AsyncOrchestrator(
@Async("regenerateThumbnailsTaskExecutor")
fun regenerateMissingThumbnails() {
logger.info { "Regenerate missing thumbnails" }
generateThumbnails(bookRepository.findAllByMetadataThumbnailIsNull())
generateThumbnails(bookRepository.findAllByMediaThumbnailIsNull())
}
private fun generateThumbnails(books: List<Book>) {

View File

@ -4,8 +4,8 @@ import mu.KotlinLogging
import net.coobird.thumbnailator.Thumbnails
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.MetadataNotReadyException
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.model.UnsupportedMediaTypeException
import org.gotson.komga.infrastructure.archive.ContentDetector
import org.gotson.komga.infrastructure.archive.PdfExtractor
@ -18,7 +18,7 @@ import java.util.*
private val logger = KotlinLogging.logger {}
@Service
class BookParser(
class BookAnalyzer(
private val contentDetector: ContentDetector,
private val zipExtractor: ZipExtractor,
private val rarExtractor: RarExtractor,
@ -37,8 +37,8 @@ class BookParser(
private val thumbnailFormat = "jpeg"
@Throws(UnsupportedMediaTypeException::class)
fun parse(book: Book): BookMetadata {
logger.info { "Trying to parse book: $book" }
fun analyze(book: Book): Media {
logger.info { "Trying to analyze book: $book" }
val mediaType = contentDetector.detectMediaType(book.path())
logger.info { "Detected media type: $mediaType" }
@ -52,24 +52,24 @@ class BookParser(
logger.info { "Trying to generate cover for book: $book" }
val thumbnail = generateThumbnail(book, mediaType, pages.first().fileName)
return BookMetadata(mediaType = mediaType, status = BookMetadata.Status.READY, pages = pages, thumbnail = thumbnail)
return Media(mediaType = mediaType, status = Media.Status.READY, pages = pages, thumbnail = thumbnail)
}
@Throws(MetadataNotReadyException::class)
fun regenerateThumbnail(book: Book): BookMetadata {
@Throws(MediaNotReadyException::class)
fun regenerateThumbnail(book: Book): Media {
logger.info { "Regenerate thumbnail for book: $book" }
if (book.metadata.status != BookMetadata.Status.READY) {
logger.warn { "Book metadata is not ready, cannot generate thumbnail. Book: $book" }
throw MetadataNotReadyException()
if (book.media.status != Media.Status.READY) {
logger.warn { "Book media is not ready, cannot generate thumbnail. Book: $book" }
throw MediaNotReadyException()
}
val thumbnail = generateThumbnail(book, book.metadata.mediaType!!, book.metadata.pages.first().fileName)
val thumbnail = generateThumbnail(book, book.media.mediaType!!, book.media.pages.first().fileName)
return BookMetadata(
mediaType = book.metadata.mediaType,
status = BookMetadata.Status.READY,
pages = book.metadata.pages,
return Media(
mediaType = book.media.mediaType,
status = Media.Status.READY,
pages = book.media.pages,
thumbnail = thumbnail
)
}
@ -91,22 +91,22 @@ class BookParser(
}
@Throws(
MetadataNotReadyException::class,
MediaNotReadyException::class,
IndexOutOfBoundsException::class
)
fun getPageContent(book: Book, number: Int): ByteArray {
logger.info { "Get page #$number for book: $book" }
if (book.metadata.status != BookMetadata.Status.READY) {
logger.warn { "Book metadata is not ready, cannot get pages" }
throw MetadataNotReadyException()
if (book.media.status != Media.Status.READY) {
logger.warn { "Book media is not ready, cannot get pages" }
throw MediaNotReadyException()
}
if (number > book.metadata.pages.size || number <= 0) {
logger.error { "Page number #$number is out of bounds. Book has ${book.metadata.pages.size} pages" }
if (number > book.media.pages.size || number <= 0) {
logger.error { "Page number #$number is out of bounds. Book has ${book.media.pages.size} pages" }
throw IndexOutOfBoundsException("Page $number does not exist")
}
return supportedMediaTypes.getValue(book.metadata.mediaType!!).getPageStream(book.path(), book.metadata.pages[number - 1].fileName)
return supportedMediaTypes.getValue(book.media.mediaType!!).getPageStream(book.path(), book.media.pages[number - 1].fileName)
}
}

View File

@ -3,9 +3,9 @@ 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.BookMetadata
import org.gotson.komga.domain.model.BookPageContent
import org.gotson.komga.domain.model.MetadataNotReadyException
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.model.UnsupportedMediaTypeException
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.infrastructure.image.ImageConverter
@ -22,38 +22,38 @@ private val logger = KotlinLogging.logger {}
@Service
class BookLifecycle(
private val bookRepository: BookRepository,
private val bookParser: BookParser,
private val bookAnalyzer: BookAnalyzer,
private val imageConverter: ImageConverter
) {
@Transactional
@Async("parseBookTaskExecutor")
fun parseAndPersist(book: Book): Future<Long> {
logger.info { "Parse and persist book: $book" }
@Async("analyzeBookTaskExecutor")
fun analyzeAndPersist(book: Book): Future<Long> {
logger.info { "Analyze and persist book: $book" }
return AsyncResult(measureTimeMillis {
try {
book.metadata = bookParser.parse(book)
book.media = bookAnalyzer.analyze(book)
} catch (ex: UnsupportedMediaTypeException) {
logger.info(ex) { "Unsupported media type: ${ex.mediaType}. Book: $book" }
book.metadata = BookMetadata(status = BookMetadata.Status.UNSUPPORTED, mediaType = ex.mediaType)
book.media = Media(status = Media.Status.UNSUPPORTED, mediaType = ex.mediaType)
} catch (ex: Exception) {
logger.error(ex) { "Error while parsing. Book: $book" }
book.metadata = BookMetadata(status = BookMetadata.Status.ERROR)
book.media = Media(status = Media.Status.ERROR)
}
bookRepository.save(book)
}.also { logger.info { "Parsing finished in ${DurationFormatUtils.formatDurationHMS(it)}" } })
}
@Transactional
@Async("parseBookTaskExecutor")
@Async("analyzeBookTaskExecutor")
fun regenerateThumbnailAndPersist(book: Book): Future<Long> {
logger.info { "Regenerate thumbnail and persist book: $book" }
return AsyncResult(measureTimeMillis {
try {
book.metadata = bookParser.regenerateThumbnail(book)
book.media = bookAnalyzer.regenerateThumbnail(book)
} catch (ex: Exception) {
logger.error(ex) { "Error while recreating thumbnail" }
book.metadata = BookMetadata(status = BookMetadata.Status.ERROR)
book.media = Media(status = Media.Status.ERROR)
}
bookRepository.save(book)
}.also { logger.info { "Thumbnail generated in ${DurationFormatUtils.formatDurationHMS(it)}" } })
@ -61,12 +61,12 @@ class BookLifecycle(
@Throws(
UnsupportedMediaTypeException::class,
MetadataNotReadyException::class,
MediaNotReadyException::class,
IndexOutOfBoundsException::class
)
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null): BookPageContent {
val pageContent = bookParser.getPageContent(book, number)
val pageMediaType = book.metadata.pages[number - 1].mediaType
val pageContent = bookAnalyzer.getPageContent(book, number)
val pageMediaType = book.media.pages[number - 1].mediaType
convertTo?.let {
val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}"

View File

@ -54,7 +54,7 @@ class LibraryLifecycle(
logger.info { "Trying to launch a scan for the newly added library: ${library.name}" }
try {
asyncOrchestrator.scanAndParse()
asyncOrchestrator.scanAndAnalyze()
} catch (e: RejectedExecutionException) {
logger.warn { "Another scan is already running, skipping" }
}

View File

@ -2,8 +2,8 @@ package org.gotson.komga.domain.service
import mu.KotlinLogging
import org.apache.commons.lang3.time.DurationFormatUtils
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.Library
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.springframework.stereotype.Service
@ -59,10 +59,10 @@ class LibraryScanner(
val existingBook = bookRepository.findByUrl(newBook.url) ?: newBook
if (newBook.fileLastModified.truncatedTo(ChronoUnit.MILLIS) != existingBook.fileLastModified.truncatedTo(ChronoUnit.MILLIS)) {
logger.info { "Book changed on disk, update and reset metadata status: $newBook" }
logger.info { "Book changed on disk, update and reset media status: $newBook" }
existingBook.fileLastModified = newBook.fileLastModified
existingBook.fileSize = newBook.fileSize
existingBook.metadata.reset()
existingBook.media.reset()
}
existingBook
}.toMutableList()
@ -74,14 +74,14 @@ class LibraryScanner(
}.also { logger.info { "Library update finished in ${DurationFormatUtils.formatDurationHMS(it)}" } }
}
fun parseUnparsedBooks() {
logger.info { "Parsing all books in status: unknown" }
val booksToParse = bookRepository.findAllByMetadataStatus(BookMetadata.Status.UNKNOWN)
fun analyzeUnknownBooks() {
logger.info { "Analyze all books in status: unknown" }
val booksToAnalyze = bookRepository.findAllByMediaStatus(Media.Status.UNKNOWN)
var sumOfTasksTime = 0L
measureTimeMillis {
sumOfTasksTime = booksToParse
.map { bookLifecycle.parseAndPersist(it) }
sumOfTasksTime = booksToAnalyze
.map { bookLifecycle.analyzeAndPersist(it) }
.map {
try {
it.get()
@ -91,7 +91,7 @@ class LibraryScanner(
}
.sum()
}.also {
logger.info { "Parsed ${booksToParse.size} books in ${DurationFormatUtils.formatDurationHMS(it)} (virtual: ${DurationFormatUtils.formatDurationHMS(sumOfTasksTime)})" }
logger.info { "Analyzed ${booksToAnalyze.size} books in ${DurationFormatUtils.formatDurationHMS(it)} (virtual: ${DurationFormatUtils.formatDurationHMS(sumOfTasksTime)})" }
}
}

View File

@ -13,10 +13,10 @@ class AsyncConfiguration(
private val komgaProperties: KomgaProperties
) {
@Bean("parseBookTaskExecutor")
fun parseBookTaskExecutor(): Executor =
@Bean("analyzeBookTaskExecutor")
fun analyzeBookTaskExecutor(): Executor =
ThreadPoolTaskExecutor().apply {
corePoolSize = komgaProperties.threads.parse
corePoolSize = komgaProperties.threads.analyzer
}
@Bean("periodicScanTaskExecutor")
@ -34,4 +34,4 @@ class AsyncConfiguration(
maxPoolSize = 1
setQueueCapacity(0)
}
}
}

View File

@ -19,6 +19,10 @@ class KomgaProperties {
class Threads {
@Min(1)
@Deprecated("Deprecated since 0.10", ReplaceWith("analyzer"))
var parse: Int = 2
@Min(1)
var analyzer: Int = 2
}
}

View File

@ -21,7 +21,7 @@ class PeriodicScannerController(
@Scheduled(cron = "#{@komgaProperties.librariesScanCron ?: '-'}")
fun scanRootFolder() {
try {
asyncOrchestrator.scanAndParse()
asyncOrchestrator.scanAndAnalyze()
} catch (e: RejectedExecutionException) {
logger.warn { "Another scan is already running, skipping" }
}

View File

@ -3,8 +3,8 @@ package org.gotson.komga.interfaces.web.opds
import com.github.klinq.jpaspec.`in`
import com.github.klinq.jpaspec.likeLower
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.Library
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.SeriesRepository
@ -209,7 +209,7 @@ class OpdsController(
OpdsLinkFeedNavigation(OpdsLinkRel.SELF, "${ROUTE_BASE}series/$id"),
linkStart
),
entries = series.books.filter { it.metadata.status == BookMetadata.Status.READY }.map { it.toOpdsEntry() }
entries = series.books.filter { it.media.status == Media.Status.READY }.map { it.toOpdsEntry() }
)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@ -252,9 +252,9 @@ class OpdsController(
content = "$name (${fileExtension().toUpperCase()}) (${fileSizeHumanReadable()})",
links = listOf(
OpdsLinkImageThumbnail("image/jpeg", "${ROUTE_BASE}books/$id/thumbnail"),
OpdsLinkImage(metadata.pages[0].mediaType, "${ROUTE_BASE}books/$id/pages/1"),
OpdsLinkFileAcquisition(metadata.mediaType, "${ROUTE_BASE}books/$id/file/${fileName()}"),
OpdsLinkPageStreaming("image/jpeg", "${ROUTE_BASE}books/$id/pages/{pageNumber}?convert=jpeg&zero_based=true", metadata.pages.size)
OpdsLinkImage(media.pages[0].mediaType, "${ROUTE_BASE}books/$id/pages/1"),
OpdsLinkFileAcquisition(media.mediaType, "${ROUTE_BASE}books/$id/file/${fileName()}"),
OpdsLinkPageStreaming("image/jpeg", "${ROUTE_BASE}books/$id/pages/{pageNumber}?convert=jpeg&zero_based=true", media.pages.size)
)
)

View File

@ -4,8 +4,8 @@ import com.github.klinq.jpaspec.`in`
import com.github.klinq.jpaspec.likeLower
import mu.KotlinLogging
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.MetadataNotReadyException
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.SeriesRepository
@ -155,10 +155,10 @@ class BookController(
.body(ByteArray(0))
}
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
if (book.metadata.thumbnail != null) {
if (book.media.thumbnail != null) {
ResponseEntity.ok()
.setNotModified(book)
.body(book.metadata.thumbnail)
.body(book.media.thumbnail)
} else throw ResponseStatusException(HttpStatus.NOT_FOUND)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@ -192,7 +192,7 @@ class BookController(
.filename(book.fileName())
.build()
})
.contentType(getMediaTypeOrDefault(book.metadata.mediaType))
.contentType(getMediaTypeOrDefault(book.media.mediaType))
.body(File(book.url.toURI()).readBytes())
} catch (ex: FileNotFoundException) {
logger.warn(ex) { "File not found: $book" }
@ -216,10 +216,10 @@ class BookController(
): List<PageDto> =
bookRepository.findByIdOrNull((bookId))?.let {
if (!principal.user.canAccessBook(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
if (it.metadata.status == BookMetadata.Status.UNKNOWN) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book has not been parsed yet")
if (it.metadata.status in listOf(BookMetadata.Status.ERROR, BookMetadata.Status.UNSUPPORTED)) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book cannot be parsed")
if (it.media.status == Media.Status.UNKNOWN) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book has not been analyzed yet")
if (it.media.status in listOf(Media.Status.ERROR, Media.Status.UNSUPPORTED)) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
it.metadata.pages.mapIndexed { index, s -> PageDto(index + 1, s.fileName, s.mediaType) }
it.media.pages.mapIndexed { index, s -> PageDto(index + 1, s.fileName, s.mediaType) }
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
@ -273,8 +273,8 @@ class BookController(
.body(pageContent.content)
} catch (ex: IndexOutOfBoundsException) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist")
} catch (ex: MetadataNotReadyException) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book cannot be parsed")
} catch (ex: MediaNotReadyException) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
} catch (ex: NoSuchFileException) {
logger.warn(ex) { "File not found: $book" }
throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved")

View File

@ -14,7 +14,11 @@ data class SeriesDto(
val name: String,
val url: String,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val lastModified: LocalDateTime?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val fileLastModified: LocalDateTime,
val booksCount: Int
)
@ -23,7 +27,9 @@ fun Series.toDto(includeUrl: Boolean) = SeriesDto(
libraryId = library.id,
name = name,
url = if (includeUrl) url.toURI().path else "",
created = createdDate?.toUTC(),
lastModified = lastModifiedDate?.toUTC(),
fileLastModified = fileLastModified.toUTC(),
booksCount = books.size
)
@ -34,13 +40,19 @@ data class BookDto(
val url: String,
val number: Float,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val lastModified: LocalDateTime?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val fileLastModified: LocalDateTime,
val sizeBytes: Long,
val size: String,
val metadata: BookMetadataDto
@Deprecated("Deprecated since 0.10", ReplaceWith("media"))
val metadata: MediaDto,
val media: MediaDto
)
data class BookMetadataDto(
data class MediaDto(
val status: String,
val mediaType: String,
val pagesCount: Int
@ -53,13 +65,20 @@ fun Book.toDto(includeFullUrl: Boolean) =
name = name,
url = if (includeFullUrl) url.toURI().path else FilenameUtils.getName(url.toURI().path),
number = number,
created = createdDate?.toUTC(),
lastModified = lastModifiedDate?.toUTC(),
fileLastModified = fileLastModified.toUTC(),
sizeBytes = fileSize,
size = fileSizeHumanReadable(),
metadata = BookMetadataDto(
status = metadata.status.toString(),
mediaType = metadata.mediaType ?: "",
pagesCount = metadata.pages.size
metadata = MediaDto(
status = media.status.toString(),
mediaType = media.mediaType ?: "",
pagesCount = media.pages.size
),
media = MediaDto(
status = media.status.toString(),
mediaType = media.mediaType ?: "",
pagesCount = media.pages.size
)
)

View File

@ -3,7 +3,7 @@ package org.gotson.komga.interfaces.web.rest
import com.github.klinq.jpaspec.`in`
import com.github.klinq.jpaspec.likeLower
import mu.KotlinLogging
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
@ -164,7 +164,9 @@ class SeriesController(
fun getAllBooksBySeries(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable(name = "seriesId") id: Long,
@RequestParam(value = "ready_only", defaultValue = "true") readyFilter: Boolean,
@RequestParam(name = "ready_only", defaultValue = "true")
readyFilter: Boolean,
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
page: Pageable
): Page<BookDto> {
seriesRepository.findByIdOrNull(id)?.let {
@ -178,8 +180,8 @@ class SeriesController(
else Sort.by(Sort.Order.asc("number"))
)
return if (readyFilter) {
bookRepository.findAllByMetadataStatusAndSeriesId(BookMetadata.Status.READY, id, pageRequest)
return if (!mediaStatus.isNullOrEmpty()) {
bookRepository.findAllByMediaStatusInAndSeriesId(mediaStatus, id, pageRequest)
} else {
bookRepository.findAllBySeriesId(id, pageRequest)
}.map { it.toDto(includeFullUrl = principal.user.isAdmin()) }

View File

@ -1,6 +1,7 @@
komga:
threads:
parse: 1
analyzer: 1
filesystem-scanner-force-directory-modified-time: false
# libraries-scan-directory-exclusions:
# - "#recycle"
# - "@eaDir"

View File

@ -43,7 +43,7 @@ caffeine.jcache {
}
}
cache.bookmetadata {
cache.media {
monitoring {
statistics = true
}
@ -54,7 +54,7 @@ caffeine.jcache {
}
}
cache.bookmetadata.collection.pages {
cache.media.collection.pages {
monitoring {
statistics = true
}

View File

@ -0,0 +1,25 @@
alter table book_metadata
rename to media;
alter table book
alter column book_metadata_id
rename to media_id;
alter table book
rename constraint uk_book_book_metadata_id to uk_book_media_id;
alter table book
rename constraint fk_book_book_metadata_book_metadata_id to fk_book_media_media_id;
alter table book_metadata_page
rename to media_page;
alter table media_page
alter column book_metadata_id
rename to media_id;
alter table media_page
rename constraint fk_book_metadata_page_book_metadata_book_metadata_id to fk_media_page_media_media_id;
alter index if exists uk_book_book_metadata_id_index_7 rename to uk_book_media_id_index_7;
alter index if exists fk_book_metadata_page_book_metadata_book_metadata_id_index_9 rename to fk_media_page_media_media_id_index_9;