mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 21:00:16 +02:00
feat: better management of book analysis errors
This commit is contained in:
parent
4afa912dd0
commit
8c26a318fe
9 changed files with 106 additions and 99 deletions
|
|
@ -84,16 +84,17 @@
|
||||||
<v-col cols="10" class="body-2">{{ book.size }}</v-col>
|
<v-col cols="10" class="body-2">{{ book.size }}</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="book.media.comment">
|
||||||
|
<v-col cols="2" md="1" lg="1" xl="1" class="body-2">COMMENT</v-col>
|
||||||
|
<v-col cols="10" class="body-2">
|
||||||
|
<span class="error--text font-weight-bold">{{ book.media.comment }}</span>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="2" md="1" lg="1" xl="1" class="body-2">FORMAT</v-col>
|
<v-col cols="2" md="1" lg="1" xl="1" class="body-2">FORMAT</v-col>
|
||||||
<v-col cols="10" class="body-2">
|
<v-col cols="10" class="body-2">
|
||||||
<template v-if="book.media.status === 'ERROR'">
|
<span>{{ format.type }}</span>
|
||||||
<span class="error--text font-weight-bold">Book analysis failed</span>
|
|
||||||
</template>
|
|
||||||
<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>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ interface BookDto {
|
||||||
interface MediaDto {
|
interface MediaDto {
|
||||||
status: string,
|
status: string,
|
||||||
mediaType: string,
|
mediaType: string,
|
||||||
pagesCount: number
|
pagesCount: number,
|
||||||
|
comment: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageDto {
|
interface PageDto {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
class MediaNotReadyException : Exception()
|
class MediaNotReadyException : Exception()
|
||||||
class EmptyBookException(val mediaType: String) : Exception()
|
class ImageConversionException(message: String) : Exception(message)
|
||||||
class UnsupportedMediaTypeException(message: String, val mediaType: String) : Exception(message)
|
|
||||||
class DirectoryNotFoundException(message: String) : Exception(message)
|
class DirectoryNotFoundException(message: String) : Exception(message)
|
||||||
class DuplicateNameException(message: String) : Exception(message)
|
class DuplicateNameException(message: String) : Exception(message)
|
||||||
class PathContainedInPath(message: String) : Exception(message)
|
class PathContainedInPath(message: String) : Exception(message)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@ class Media(
|
||||||
@Lob
|
@Lob
|
||||||
var thumbnail: ByteArray? = null,
|
var thumbnail: ByteArray? = null,
|
||||||
|
|
||||||
pages: Iterable<BookPage> = emptyList()
|
pages: Iterable<BookPage> = emptyList(),
|
||||||
|
|
||||||
|
@Column(name = "comment")
|
||||||
|
var comment: String? = null
|
||||||
) : AuditableEntity() {
|
) : AuditableEntity() {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ import mu.KotlinLogging
|
||||||
import net.coobird.thumbnailator.Thumbnails
|
import net.coobird.thumbnailator.Thumbnails
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||||
import org.gotson.komga.domain.model.Book
|
import org.gotson.komga.domain.model.Book
|
||||||
import org.gotson.komga.domain.model.EmptyBookException
|
|
||||||
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
|
||||||
import org.gotson.komga.domain.model.UnsupportedMediaTypeException
|
|
||||||
import org.gotson.komga.infrastructure.archive.ContentDetector
|
import org.gotson.komga.infrastructure.archive.ContentDetector
|
||||||
import org.gotson.komga.infrastructure.archive.PdfExtractor
|
import org.gotson.komga.infrastructure.archive.PdfExtractor
|
||||||
import org.gotson.komga.infrastructure.archive.RarExtractor
|
import org.gotson.komga.infrastructure.archive.RarExtractor
|
||||||
|
|
@ -37,21 +35,25 @@ class BookAnalyzer(
|
||||||
private val thumbnailSize = 300
|
private val thumbnailSize = 300
|
||||||
private val thumbnailFormat = "jpeg"
|
private val thumbnailFormat = "jpeg"
|
||||||
|
|
||||||
@Throws(
|
|
||||||
UnsupportedMediaTypeException::class,
|
|
||||||
EmptyBookException::class
|
|
||||||
)
|
|
||||||
fun analyze(book: Book): Media {
|
fun analyze(book: Book): Media {
|
||||||
logger.info { "Trying to analyze book: $book" }
|
logger.info { "Trying to analyze book: $book" }
|
||||||
|
|
||||||
val mediaType = contentDetector.detectMediaType(book.path())
|
val mediaType = contentDetector.detectMediaType(book.path())
|
||||||
logger.info { "Detected media type: $mediaType" }
|
logger.info { "Detected media type: $mediaType" }
|
||||||
if (!supportedMediaTypes.keys.contains(mediaType))
|
if (!supportedMediaTypes.keys.contains(mediaType))
|
||||||
throw UnsupportedMediaTypeException("Unsupported mime type: $mediaType. File: $book", mediaType)
|
return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = "Media type $mediaType is not supported")
|
||||||
|
|
||||||
val pages = supportedMediaTypes.getValue(mediaType).getPagesList(book.path())
|
val pages = try {
|
||||||
.sortedWith(compareBy(natSortComparator) { it.fileName })
|
supportedMediaTypes.getValue(mediaType).getPagesList(book.path()).sortedWith(compareBy(natSortComparator) { it.fileName })
|
||||||
if (pages.isEmpty()) throw EmptyBookException(mediaType)
|
} catch (ex: Exception) {
|
||||||
|
logger.error(ex) { "Error while analyzing book: $book" }
|
||||||
|
return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = ex.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
logger.warn { "Book $book does not contain any pages" }
|
||||||
|
return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "Book does not contain any pages")
|
||||||
|
}
|
||||||
logger.info { "Book has ${pages.size} pages" }
|
logger.info { "Book has ${pages.size} pages" }
|
||||||
|
|
||||||
logger.info { "Trying to generate cover for book: $book" }
|
logger.info { "Trying to generate cover for book: $book" }
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ import mu.KotlinLogging
|
||||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||||
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.EmptyBookException
|
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
|
||||||
import org.gotson.komga.domain.model.UnsupportedMediaTypeException
|
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.infrastructure.image.ImageConverter
|
import org.gotson.komga.infrastructure.image.ImageConverter
|
||||||
import org.gotson.komga.infrastructure.image.ImageType
|
import org.gotson.komga.infrastructure.image.ImageType
|
||||||
|
|
@ -34,15 +33,9 @@ class BookLifecycle(
|
||||||
return AsyncResult(measureTimeMillis {
|
return AsyncResult(measureTimeMillis {
|
||||||
try {
|
try {
|
||||||
book.media = bookAnalyzer.analyze(book)
|
book.media = bookAnalyzer.analyze(book)
|
||||||
} catch (ex: UnsupportedMediaTypeException) {
|
|
||||||
logger.warn { "Unsupported media type: ${ex.mediaType}. Book: $book" }
|
|
||||||
book.media = Media(status = Media.Status.UNSUPPORTED, mediaType = ex.mediaType)
|
|
||||||
} catch (ex: EmptyBookException) {
|
|
||||||
logger.warn { "Book does not contain any images: $book" }
|
|
||||||
book.media = Media(status = Media.Status.ERROR, mediaType = ex.mediaType)
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
logger.error(ex) { "Error while parsing. Book: $book" }
|
logger.error(ex) { "Error while analyzing book: $book" }
|
||||||
book.media = Media(status = Media.Status.ERROR)
|
book.media = Media(status = Media.Status.ERROR, comment = ex.message)
|
||||||
}
|
}
|
||||||
bookRepository.save(book)
|
bookRepository.save(book)
|
||||||
}.also { logger.info { "Parsing finished in ${DurationFormatUtils.formatDurationHMS(it)}" } })
|
}.also { logger.info { "Parsing finished in ${DurationFormatUtils.formatDurationHMS(it)}" } })
|
||||||
|
|
@ -64,9 +57,9 @@ class BookLifecycle(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(
|
@Throws(
|
||||||
UnsupportedMediaTypeException::class,
|
ImageConversionException::class,
|
||||||
MediaNotReadyException::class,
|
MediaNotReadyException::class,
|
||||||
IndexOutOfBoundsException::class
|
IndexOutOfBoundsException::class
|
||||||
)
|
)
|
||||||
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null): BookPageContent {
|
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null): BookPageContent {
|
||||||
val pageContent = bookAnalyzer.getPageContent(book, number)
|
val pageContent = bookAnalyzer.getPageContent(book, number)
|
||||||
|
|
@ -75,10 +68,10 @@ class BookLifecycle(
|
||||||
convertTo?.let {
|
convertTo?.let {
|
||||||
val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}"
|
val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}"
|
||||||
if (!imageConverter.supportedReadMediaTypes.contains(pageMediaType)) {
|
if (!imageConverter.supportedReadMediaTypes.contains(pageMediaType)) {
|
||||||
throw UnsupportedMediaTypeException("$msg: unsupported read format $pageMediaType", pageMediaType)
|
throw ImageConversionException("$msg: unsupported read format $pageMediaType")
|
||||||
}
|
}
|
||||||
if (!imageConverter.supportedWriteMediaTypes.contains(it.mediaType)) {
|
if (!imageConverter.supportedWriteMediaTypes.contains(it.mediaType)) {
|
||||||
throw UnsupportedMediaTypeException("$msg: unsupported cannot write format ${it.mediaType}", it.mediaType)
|
throw ImageConversionException("$msg: unsupported write format ${it.mediaType}")
|
||||||
}
|
}
|
||||||
if (pageMediaType == it.mediaType) {
|
if (pageMediaType == it.mediaType) {
|
||||||
logger.warn { "$msg: same format, no need for conversion" }
|
logger.warn { "$msg: same format, no need for conversion" }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import com.github.klinq.jpaspec.`in`
|
||||||
import com.github.klinq.jpaspec.likeLower
|
import com.github.klinq.jpaspec.likeLower
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.gotson.komga.domain.model.Book
|
import org.gotson.komga.domain.model.Book
|
||||||
|
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
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
|
|
@ -227,6 +228,8 @@ class BookController(
|
||||||
.body(pageContent.content)
|
.body(pageContent.content)
|
||||||
} catch (ex: IndexOutOfBoundsException) {
|
} catch (ex: IndexOutOfBoundsException) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist")
|
||||||
|
} catch (ex: ImageConversionException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, ex.message)
|
||||||
} catch (ex: MediaNotReadyException) {
|
} catch (ex: MediaNotReadyException) {
|
||||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
|
||||||
} catch (ex: NoSuchFileException) {
|
} catch (ex: NoSuchFileException) {
|
||||||
|
|
|
||||||
|
|
@ -9,84 +9,87 @@ import java.time.ZoneId
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
data class SeriesDto(
|
data class SeriesDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val libraryId: Long,
|
val libraryId: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val created: LocalDateTime?,
|
val created: LocalDateTime?,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val lastModified: LocalDateTime?,
|
val lastModified: LocalDateTime?,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val fileLastModified: LocalDateTime,
|
val fileLastModified: LocalDateTime,
|
||||||
val booksCount: Int
|
val booksCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Series.toDto(includeUrl: Boolean) = SeriesDto(
|
fun Series.toDto(includeUrl: Boolean) = SeriesDto(
|
||||||
id = id,
|
id = id,
|
||||||
libraryId = library.id,
|
libraryId = library.id,
|
||||||
name = name,
|
name = name,
|
||||||
url = if (includeUrl) url.toURI().path else "",
|
url = if (includeUrl) url.toURI().path else "",
|
||||||
created = createdDate?.toUTC(),
|
created = createdDate?.toUTC(),
|
||||||
lastModified = lastModifiedDate?.toUTC(),
|
lastModified = lastModifiedDate?.toUTC(),
|
||||||
fileLastModified = fileLastModified.toUTC(),
|
fileLastModified = fileLastModified.toUTC(),
|
||||||
booksCount = books.size
|
booksCount = books.size
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BookDto(
|
data class BookDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val seriesId: Long,
|
val seriesId: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val number: Float,
|
val number: Float,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val created: LocalDateTime?,
|
val created: LocalDateTime?,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val lastModified: LocalDateTime?,
|
val lastModified: LocalDateTime?,
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
val fileLastModified: LocalDateTime,
|
val fileLastModified: LocalDateTime,
|
||||||
val sizeBytes: Long,
|
val sizeBytes: Long,
|
||||||
val size: String,
|
val size: String,
|
||||||
@Deprecated("Deprecated since 0.10", ReplaceWith("media"))
|
@Deprecated("Deprecated since 0.10", ReplaceWith("media"))
|
||||||
val metadata: MediaDto,
|
val metadata: MediaDto,
|
||||||
val media: MediaDto
|
val media: MediaDto
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MediaDto(
|
data class MediaDto(
|
||||||
val status: String,
|
val status: String,
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val pagesCount: Int
|
val pagesCount: Int,
|
||||||
|
val comment: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Book.toDto(includeFullUrl: Boolean) =
|
fun Book.toDto(includeFullUrl: Boolean) =
|
||||||
BookDto(
|
BookDto(
|
||||||
id = id,
|
id = id,
|
||||||
seriesId = series.id,
|
seriesId = series.id,
|
||||||
name = name,
|
name = name,
|
||||||
url = if (includeFullUrl) url.toURI().path else FilenameUtils.getName(url.toURI().path),
|
url = if (includeFullUrl) url.toURI().path else FilenameUtils.getName(url.toURI().path),
|
||||||
number = number,
|
number = number,
|
||||||
created = createdDate?.toUTC(),
|
created = createdDate?.toUTC(),
|
||||||
lastModified = lastModifiedDate?.toUTC(),
|
lastModified = lastModifiedDate?.toUTC(),
|
||||||
fileLastModified = fileLastModified.toUTC(),
|
fileLastModified = fileLastModified.toUTC(),
|
||||||
sizeBytes = fileSize,
|
sizeBytes = fileSize,
|
||||||
size = fileSizeHumanReadable(),
|
size = fileSizeHumanReadable(),
|
||||||
metadata = MediaDto(
|
metadata = MediaDto(
|
||||||
status = media.status.toString(),
|
status = media.status.toString(),
|
||||||
mediaType = media.mediaType ?: "",
|
mediaType = media.mediaType ?: "",
|
||||||
pagesCount = media.pages.size
|
pagesCount = media.pages.size,
|
||||||
),
|
comment = media.comment ?: ""
|
||||||
media = MediaDto(
|
),
|
||||||
status = media.status.toString(),
|
media = MediaDto(
|
||||||
mediaType = media.mediaType ?: "",
|
status = media.status.toString(),
|
||||||
pagesCount = media.pages.size
|
mediaType = media.mediaType ?: "",
|
||||||
)
|
pagesCount = media.pages.size,
|
||||||
|
comment = media.comment ?: ""
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
data class PageDto(
|
data class PageDto(
|
||||||
val number: Int,
|
val number: Int,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
val mediaType: String
|
val mediaType: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun LocalDateTime.toUTC(): LocalDateTime =
|
fun LocalDateTime.toUTC(): LocalDateTime =
|
||||||
atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table media
|
||||||
|
add (comment varchar);
|
||||||
Loading…
Reference in a new issue