mirror of
https://github.com/gotson/komga.git
synced 2025-01-09 04:08:00 +08:00
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:
parent
38be19de33
commit
ebad597f26
@ -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
|
||||
|
||||
|
@ -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') {
|
||||
|
@ -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) {
|
||||
|
@ -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':
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 () {
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
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)
|
||||
|
@ -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>
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
@ -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>) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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}"
|
||||
|
@ -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" }
|
||||
}
|
||||
|
@ -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)})" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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" }
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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()) }
|
||||
|
@ -1,6 +1,7 @@
|
||||
komga:
|
||||
threads:
|
||||
parse: 1
|
||||
analyzer: 1
|
||||
filesystem-scanner-force-directory-modified-time: false
|
||||
# libraries-scan-directory-exclusions:
|
||||
# - "#recycle"
|
||||
# - "@eaDir"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user