diff --git a/komga-webui/src/components/BrowseBook.vue b/komga-webui/src/components/BrowseBook.vue index b811d12cf..dfe2b82a1 100644 --- a/komga-webui/src/components/BrowseBook.vue +++ b/komga-webui/src/components/BrowseBook.vue @@ -84,16 +84,17 @@ {{ book.size }} + + COMMENT + + {{ book.media.comment }} + + + FORMAT - - - + {{ format.type }} diff --git a/komga-webui/src/types/komga-books.ts b/komga-webui/src/types/komga-books.ts index f0a32aad8..1e8cf976f 100644 --- a/komga-webui/src/types/komga-books.ts +++ b/komga-webui/src/types/komga-books.ts @@ -13,7 +13,8 @@ interface BookDto { interface MediaDto { status: string, mediaType: string, - pagesCount: number + pagesCount: number, + comment: string } interface PageDto { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt index 04c091800..8e5dbaa5e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt @@ -1,8 +1,7 @@ package org.gotson.komga.domain.model class MediaNotReadyException : Exception() -class EmptyBookException(val mediaType: String) : Exception() -class UnsupportedMediaTypeException(message: String, val mediaType: String) : Exception(message) +class ImageConversionException(message: String) : Exception(message) class DirectoryNotFoundException(message: String) : Exception(message) class DuplicateNameException(message: String) : Exception(message) class PathContainedInPath(message: String) : Exception(message) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Media.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Media.kt index f5442e94d..3025d5613 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Media.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Media.kt @@ -37,7 +37,10 @@ class Media( @Lob var thumbnail: ByteArray? = null, - pages: Iterable = emptyList() + pages: Iterable = emptyList(), + + @Column(name = "comment") + var comment: String? = null ) : AuditableEntity() { @Id @GeneratedValue diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt index 2b2e67993..834c299fc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt @@ -4,10 +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.EmptyBookException 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 import org.gotson.komga.infrastructure.archive.RarExtractor @@ -37,21 +35,25 @@ class BookAnalyzer( private val thumbnailSize = 300 private val thumbnailFormat = "jpeg" - @Throws( - UnsupportedMediaTypeException::class, - EmptyBookException::class - ) fun analyze(book: Book): Media { logger.info { "Trying to analyze book: $book" } val mediaType = contentDetector.detectMediaType(book.path()) logger.info { "Detected media type: $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()) - .sortedWith(compareBy(natSortComparator) { it.fileName }) - if (pages.isEmpty()) throw EmptyBookException(mediaType) + val pages = try { + supportedMediaTypes.getValue(mediaType).getPagesList(book.path()).sortedWith(compareBy(natSortComparator) { it.fileName }) + } 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 { "Trying to generate cover for book: $book" } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt index 6a6df2728..1818aebfc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt @@ -4,10 +4,9 @@ import mu.KotlinLogging import org.apache.commons.lang3.time.DurationFormatUtils import org.gotson.komga.domain.model.Book 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.MediaNotReadyException -import org.gotson.komga.domain.model.UnsupportedMediaTypeException import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.infrastructure.image.ImageConverter import org.gotson.komga.infrastructure.image.ImageType @@ -34,15 +33,9 @@ class BookLifecycle( return AsyncResult(measureTimeMillis { try { 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) { - logger.error(ex) { "Error while parsing. Book: $book" } - book.media = Media(status = Media.Status.ERROR) + logger.error(ex) { "Error while analyzing book: $book" } + book.media = Media(status = Media.Status.ERROR, comment = ex.message) } bookRepository.save(book) }.also { logger.info { "Parsing finished in ${DurationFormatUtils.formatDurationHMS(it)}" } }) @@ -64,9 +57,9 @@ class BookLifecycle( } @Throws( - UnsupportedMediaTypeException::class, - MediaNotReadyException::class, - IndexOutOfBoundsException::class + ImageConversionException::class, + MediaNotReadyException::class, + IndexOutOfBoundsException::class ) fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null): BookPageContent { val pageContent = bookAnalyzer.getPageContent(book, number) @@ -75,10 +68,10 @@ class BookLifecycle( convertTo?.let { val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}" 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)) { - throw UnsupportedMediaTypeException("$msg: unsupported cannot write format ${it.mediaType}", it.mediaType) + throw ImageConversionException("$msg: unsupported write format ${it.mediaType}") } if (pageMediaType == it.mediaType) { logger.warn { "$msg: same format, no need for conversion" } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index b2a1c0afb..43753a4c3 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -4,6 +4,7 @@ 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.ImageConversionException import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.persistence.BookRepository @@ -227,6 +228,8 @@ class BookController( .body(pageContent.content) } catch (ex: IndexOutOfBoundsException) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist") + } catch (ex: ImageConversionException) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, ex.message) } catch (ex: MediaNotReadyException) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed") } catch (ex: NoSuchFileException) { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/Dto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/Dto.kt index d1fd3e2bf..f0d58bf04 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/Dto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/Dto.kt @@ -9,84 +9,87 @@ import java.time.ZoneId import java.time.ZoneOffset data class SeriesDto( - val id: Long, - val libraryId: Long, - 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 + val id: Long, + val libraryId: Long, + 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 ) fun Series.toDto(includeUrl: Boolean) = SeriesDto( - id = id, - libraryId = library.id, - name = name, - url = if (includeUrl) url.toURI().path else "", - created = createdDate?.toUTC(), - lastModified = lastModifiedDate?.toUTC(), - fileLastModified = fileLastModified.toUTC(), - booksCount = books.size + id = id, + libraryId = library.id, + name = name, + url = if (includeUrl) url.toURI().path else "", + created = createdDate?.toUTC(), + lastModified = lastModifiedDate?.toUTC(), + fileLastModified = fileLastModified.toUTC(), + booksCount = books.size ) data class BookDto( - val id: Long, - val seriesId: Long, - val name: String, - 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, - @Deprecated("Deprecated since 0.10", ReplaceWith("media")) - val metadata: MediaDto, - val media: MediaDto + val id: Long, + val seriesId: Long, + val name: String, + 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, + @Deprecated("Deprecated since 0.10", ReplaceWith("media")) + val metadata: MediaDto, + val media: MediaDto ) data class MediaDto( - val status: String, - val mediaType: String, - val pagesCount: Int + val status: String, + val mediaType: String, + val pagesCount: Int, + val comment: String ) fun Book.toDto(includeFullUrl: Boolean) = - BookDto( - id = id, - seriesId = series.id, - 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 = 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 - ) + BookDto( + id = id, + seriesId = series.id, + 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 = MediaDto( + status = media.status.toString(), + mediaType = media.mediaType ?: "", + pagesCount = media.pages.size, + comment = media.comment ?: "" + ), + media = MediaDto( + status = media.status.toString(), + mediaType = media.mediaType ?: "", + pagesCount = media.pages.size, + comment = media.comment ?: "" ) + ) data class PageDto( - val number: Int, - val fileName: String, - val mediaType: String + val number: Int, + val fileName: String, + val mediaType: String ) fun LocalDateTime.toUTC(): LocalDateTime = - atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime() + atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime() diff --git a/komga/src/main/resources/db/migration/V20200108103325__media_comment.sql b/komga/src/main/resources/db/migration/V20200108103325__media_comment.sql new file mode 100644 index 000000000..a1f9b8ef6 --- /dev/null +++ b/komga/src/main/resources/db/migration/V20200108103325__media_comment.sql @@ -0,0 +1,2 @@ +alter table media + add (comment varchar);