diff --git a/komga-webui/src/components/dialogs/LibraryEditDialog.vue b/komga-webui/src/components/dialogs/LibraryEditDialog.vue index 38100e379..0a4f9b7e1 100644 --- a/komga-webui/src/components/dialogs/LibraryEditDialog.vue +++ b/komga-webui/src/components/dialogs/LibraryEditDialog.vue @@ -92,12 +92,23 @@ hide-details class="mx-4" /> + + > + + + + + + {{ $t('dialog.edit_library.label_analysis') }} + + + + + + + + + + + + + - {{ $t('dialog.edit_library.label_series_cover') }} + {{ + $t('dialog.edit_library.label_series_cover') + }} - {{ $t('dialog.edit_library.label_import_mylar') }} + {{ + $t('dialog.edit_library.label_import_mylar') + }} + > + + @@ -307,13 +383,16 @@ export default Vue.extend({ importEpubSeries: true, importMylarSeries: true, importLocalArtwork: true, - importBarcodeIsbn: true, + importBarcodeIsbn: false, scanForceModifiedTime: false, scanDeep: false, repairExtensions: false, convertToCbz: false, emptyTrashAfterScan: false, seriesCover: SeriesCoverDto.FIRST as SeriesCoverDto, + hashFiles: true, + hashPages: false, + analyzeDimensions: true, }, validationFieldNames: new Map([]), } @@ -435,13 +514,16 @@ export default Vue.extend({ this.form.importEpubSeries = library ? library.importEpubSeries : true this.form.importMylarSeries = library ? library.importMylarSeries : true this.form.importLocalArtwork = library ? library.importLocalArtwork : true - this.form.importBarcodeIsbn = library ? library.importBarcodeIsbn : true + this.form.importBarcodeIsbn = library ? library.importBarcodeIsbn : false this.form.scanForceModifiedTime = library ? library.scanForceModifiedTime : false this.form.scanDeep = library ? library.scanDeep : false this.form.repairExtensions = library ? library.repairExtensions : false this.form.convertToCbz = library ? library.convertToCbz : false this.form.emptyTrashAfterScan = library ? library.emptyTrashAfterScan : false this.form.seriesCover = library ? library.seriesCover : SeriesCoverDto.FIRST + this.form.hashFiles = library ? library.hashFiles : true + this.form.hashPages = library ? library.hashPages : false + this.form.analyzeDimensions = library ? library.analyzeDimensions : true this.$v.$reset() }, validateLibrary() { @@ -466,6 +548,9 @@ export default Vue.extend({ convertToCbz: this.form.convertToCbz, emptyTrashAfterScan: this.form.emptyTrashAfterScan, seriesCover: this.form.seriesCover, + hashFiles: this.form.hashFiles, + hashPages: this.form.hashPages, + analyzeDimensions: this.form.analyzeDimensions, } } return null diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index c26e1f5b2..871bf41a1 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -363,6 +363,9 @@ "button_next": "Next", "dialog_title_add": "Add Library", "dialot_title_edit": "Edit Library", + "field_analysis_analyze_dimensions": "Analyze pages dimensions", + "field_analysis_hash_files": "Compute hash for files", + "field_analysis_hash_pages": "Compute hash for pages", "field_convert_to_cbz": "Automatically convert to CBZ", "field_import_barcode_isbn": "ISBN barcode", "field_import_comicinfo_book": "Book metadata", @@ -382,6 +385,7 @@ "field_series_cover": "Series cover", "file_browser_dialog_button_confirm": "Choose", "file_browser_dialog_title": "Library's root folder", + "label_analysis": "Analysis", "label_file_management": "File management", "label_import_barcode_isbn": "Import ISBN within barcode", "label_import_comicinfo": "Import metadata for CBR/CBZ containing a ComicInfo.xml file", @@ -392,7 +396,9 @@ "label_series_cover": "Series cover", "tab_general": "General", "tab_metadata": "Metadata", - "tab_options": "Options" + "tab_options": "Options", + "tooltip_scanner_force_modified_time": "Enable if the library is on a Google Drive", + "tooltip_use_resources": "Can consume lots of resources on large libraries or slow hardware" }, "edit_readlist": { "button_cancel": "Cancel", diff --git a/komga-webui/src/types/komga-libraries.ts b/komga-webui/src/types/komga-libraries.ts index 32334c53b..e36dc4524 100644 --- a/komga-webui/src/types/komga-libraries.ts +++ b/komga-webui/src/types/komga-libraries.ts @@ -18,6 +18,9 @@ export interface LibraryCreationDto { convertToCbz: boolean, emptyTrashAfterScan: boolean, seriesCover: SeriesCoverDto, + hashFiles: boolean, + hashPages: boolean, + analyzeDimensions: boolean, } export interface LibraryUpdateDto { @@ -38,6 +41,9 @@ export interface LibraryUpdateDto { convertToCbz: boolean, emptyTrashAfterScan: boolean, seriesCover: SeriesCoverDto, + hashFiles: boolean, + hashPages: boolean, + analyzeDimensions: boolean, } export interface LibraryDto { @@ -59,5 +65,8 @@ export interface LibraryDto { convertToCbz: boolean, emptyTrashAfterScan: boolean, seriesCover: SeriesCoverDto, + hashFiles: boolean, + hashPages: boolean, + analyzeDimensions: boolean, unavailable: boolean, } diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index 1597708dd..5f9c6e7da 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -228,6 +228,7 @@ val migrationDirsSqlite = listOf( flyway { url = dbSqlite["url"] locations = arrayOf("classpath:db/migration/sqlite") + placeholders = mapOf("library-file-hashing" to "true") } tasks.flywayMigrate { // in order to include the Java migrations, flywayClasses must be run before flywayMigrate diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20220105143154__library_hashing_options.sql b/komga/src/flyway/resources/db/migration/sqlite/V20220105143154__library_hashing_options.sql new file mode 100644 index 000000000..14bb55a4d --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20220105143154__library_hashing_options.sql @@ -0,0 +1,6 @@ +alter table library + add column HASH_FILES boolean NOT NULL DEFAULT ${library-file-hashing}; +alter table library + add column HASH_PAGES boolean NOT NULL DEFAULT 0; +alter table library + add column ANALYZE_DIMENSIONS boolean NOT NULL DEFAULT 1; diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt index cfe893000..4ecc70dc1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt @@ -66,16 +66,17 @@ class TaskReceiver( } fun hashBooksWithoutHash(library: Library) { - if (komgaProperties.fileHashing) + if (library.hashFiles) bookRepository.findAllIdsByLibraryIdAndWithEmptyHash(library.id).forEach { submitTask(Task.HashBook(it, LOWEST_PRIORITY)) } } fun hashBookPagesWithMissingHash(library: Library) { - mediaRepository.findAllBookIdsByLibraryIdAndWithMissingPageHash(library.id, komgaProperties.pageHashing).forEach { - submitTask(Task.HashBookPages(it, LOWEST_PRIORITY)) - } + if (library.hashPages) + mediaRepository.findAllBookIdsByLibraryIdAndWithMissingPageHash(library.id, komgaProperties.pageHashing).forEach { + submitTask(Task.HashBookPages(it, LOWEST_PRIORITY)) + } } fun convertBooksToCbz(library: Library, priority: Int = DEFAULT_PRIORITY) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt index 06631d653..672502e09 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt @@ -25,6 +25,9 @@ data class Library( val convertToCbz: Boolean = false, val emptyTrashAfterScan: Boolean = false, val seriesCover: SeriesCover = SeriesCover.FIRST, + val hashFiles: Boolean = true, + val hashPages: Boolean = false, + val analyzeDimensions: Boolean = true, val unavailableDate: LocalDateTime? = null, 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 ab2f65d8f..e94b11369 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 @@ -38,7 +38,7 @@ class BookAnalyzer( private val thumbnailSize = 300 private val thumbnailFormat = "jpeg" - fun analyze(book: Book): Media { + fun analyze(book: Book, analyzeDimensions: Boolean): Media { logger.info { "Trying to analyze book: $book" } try { val mediaType = contentDetector.detectMediaType(book.path) @@ -47,7 +47,7 @@ class BookAnalyzer( return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = "ERR_1001", bookId = book.id) val entries = try { - supportedMediaTypes.getValue(mediaType).getEntries(book.path) + supportedMediaTypes.getValue(mediaType).getEntries(book.path, analyzeDimensions) } catch (ex: MediaUnsupportedException) { return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = ex.code, bookId = book.id) } catch (ex: Exception) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookConverter.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookConverter.kt index 8cadb1d97..0c9551acc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookConverter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookConverter.kt @@ -100,7 +100,7 @@ class BookConverter( ) ?: throw IllegalStateException("Newly converted book could not be scanned: $destinationFilename") - val convertedMedia = bookAnalyzer.analyze(convertedBook) + val convertedMedia = bookAnalyzer.analyze(convertedBook, libraryRepository.findById(book.libraryId).analyzeDimensions) try { when { 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 017a5ee56..0ebd02bfc 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 @@ -16,11 +16,11 @@ import org.gotson.komga.domain.model.ThumbnailBook import org.gotson.komga.domain.model.withCode import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.MediaRepository import org.gotson.komga.domain.persistence.ReadListRepository import org.gotson.komga.domain.persistence.ReadProgressRepository import org.gotson.komga.domain.persistence.ThumbnailBookRepository -import org.gotson.komga.infrastructure.configuration.KomgaProperties import org.gotson.komga.infrastructure.hash.Hasher import org.gotson.komga.infrastructure.image.ImageConverter import org.gotson.komga.infrastructure.image.ImageType @@ -47,17 +47,17 @@ class BookLifecycle( private val readProgressRepository: ReadProgressRepository, private val thumbnailBookRepository: ThumbnailBookRepository, private val readListRepository: ReadListRepository, + private val libraryRepository: LibraryRepository, private val bookAnalyzer: BookAnalyzer, private val imageConverter: ImageConverter, private val eventPublisher: EventPublisher, private val transactionTemplate: TransactionTemplate, private val hasher: Hasher, - private val komgaProperties: KomgaProperties, ) { fun analyzeAndPersist(book: Book): Boolean { logger.info { "Analyze and persist book: $book" } - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, libraryRepository.findById(book.libraryId).analyzeDimensions) transactionTemplate.executeWithoutResult { // if the number of pages has changed, delete all read progress for that book @@ -77,8 +77,8 @@ class BookLifecycle( } fun hashAndPersist(book: Book) { - if (!komgaProperties.fileHashing) - return logger.info { "File hashing is disabled, it may have changed since the task was submitted, skipping" } + if (!libraryRepository.findById(book.libraryId).hashFiles) + return logger.info { "File hashing is disabled for the library, it may have changed since the task was submitted, skipping" } logger.info { "Hash and persist book: $book" } if (book.fileHash.isBlank()) { @@ -90,6 +90,9 @@ class BookLifecycle( } fun hashPagesAndPersist(book: Book) { + if (!libraryRepository.findById(book.libraryId).hashPages) + return logger.info { "Page hashing is disabled for the library, it may have changed since the task was submitted, skipping" } + logger.info { "Hash and persist pages for book: $book" } mediaRepository.update(bookAnalyzer.hashPages(BookWithMedia(book, mediaRepository.findById(book.id)))) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt index cafbda65e..9e0743eb8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt @@ -33,7 +33,7 @@ class TransientBookLifecycle( } fun analyzeAndPersist(transientBook: BookWithMedia): BookWithMedia { - val media = bookAnalyzer.analyze(transientBook.book) + val media = bookAnalyzer.analyze(transientBook.book, true) val updated = transientBook.copy(media = media) transientBookRepository.save(updated) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt index 7ecc8a08a..fd877bad6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt @@ -23,6 +23,7 @@ class KomgaProperties { var deleteEmptyCollections: Boolean = true + @Deprecated("Deprecated since 0.143.0, you can configure this in the library options directly") var fileHashing: Boolean = true @Positive diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt index fd2b7dcf1..fc87c59a0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt @@ -75,8 +75,11 @@ class LibraryDao( .set(l.REPAIR_EXTENSIONS, library.repairExtensions) .set(l.CONVERT_TO_CBZ, library.convertToCbz) .set(l.EMPTY_TRASH_AFTER_SCAN, library.emptyTrashAfterScan) - .set(l.UNAVAILABLE_DATE, library.unavailableDate) .set(l.SERIES_COVER, library.seriesCover.toString()) + .set(l.HASH_FILES, library.hashFiles) + .set(l.HASH_PAGES, library.hashPages) + .set(l.ANALYZE_DIMENSIONS, library.analyzeDimensions) + .set(l.UNAVAILABLE_DATE, library.unavailableDate) .execute() } @@ -100,6 +103,9 @@ class LibraryDao( .set(l.CONVERT_TO_CBZ, library.convertToCbz) .set(l.EMPTY_TRASH_AFTER_SCAN, library.emptyTrashAfterScan) .set(l.SERIES_COVER, library.seriesCover.toString()) + .set(l.HASH_FILES, library.hashFiles) + .set(l.HASH_PAGES, library.hashPages) + .set(l.ANALYZE_DIMENSIONS, library.analyzeDimensions) .set(l.UNAVAILABLE_DATE, library.unavailableDate) .set(l.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z"))) .where(l.ID.eq(library.id)) @@ -127,6 +133,10 @@ class LibraryDao( convertToCbz = convertToCbz, emptyTrashAfterScan = emptyTrashAfterScan, seriesCover = Library.SeriesCover.valueOf(seriesCover), + hashFiles = hashFiles, + hashPages = hashPages, + analyzeDimensions = analyzeDimensions, + unavailableDate = unavailableDate, id = id, createdDate = createdDate.toCurrentTimeZone(), diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt index 6827c7fef..264b8af7f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt @@ -22,7 +22,7 @@ class EpubExtractor( override fun mediaTypes(): List = listOf("application/epub+zip") - override fun getEntries(path: Path): List { + override fun getEntries(path: Path, analyzeDimensions: Boolean): List { ZipFile(path.toFile()).use { zip -> try { val opfFile = getPackagePath(zip) @@ -56,7 +56,7 @@ class EpubExtractor( val mediaType = manifest.values.first { it.href == (opfDir?.relativize(image) ?: image).invariantSeparatorsPathString }.mediaType - val dimension = if (contentDetector.isImage(mediaType)) + val dimension = if (analyzeDimensions && contentDetector.isImage(mediaType)) zip.getInputStream(zip.getEntry(name)).use { imageAnalyzer.getDimension(it) } else null @@ -64,7 +64,7 @@ class EpubExtractor( } } catch (e: Exception) { logger.error(e) { "File is not a proper Epub, treating it as a zip file" } - return zipExtractor.getEntries(path) + return zipExtractor.getEntries(path, analyzeDimensions) } } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt index 9ba72dc6b..e71314af7 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt @@ -8,7 +8,7 @@ interface MediaContainerExtractor { fun mediaTypes(): List @Throws(MediaUnsupportedException::class) - fun getEntries(path: Path): List + fun getEntries(path: Path, analyzeDimensions: Boolean): List fun getEntryStream(path: Path, entryName: String): ByteArray } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt index 1f0240c16..c5ad65427 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt @@ -35,12 +35,12 @@ class PdfExtractor( override fun mediaTypes(): List = listOf("application/pdf") - override fun getEntries(path: Path): List = + override fun getEntries(path: Path, analyzeDimensions: Boolean): List = PDDocument.load(path.toFile()).use { pdf -> (0 until pdf.numberOfPages).map { index -> val page = pdf.getPage(index) val scale = page.getScale() - val dimension = Dimension((page.cropBox.width * scale).roundToInt(), (page.cropBox.height * scale).roundToInt()) + val dimension = if (analyzeDimensions) Dimension((page.cropBox.width * scale).roundToInt(), (page.cropBox.height * scale).roundToInt()) else null MediaContainerEntry(name = index.toString(), mediaType = mediaType, dimension = dimension) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt index 085484a16..d230ed458 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt @@ -21,7 +21,7 @@ class RarExtractor( override fun mediaTypes(): List = listOf("application/x-rar-compressed", "application/x-rar-compressed; version=4") - override fun getEntries(path: Path): List = + override fun getEntries(path: Path, analyzeDimensions: Boolean): List = Archive(path.toFile()).use { rar -> if (rar.isPasswordProtected) throw MediaUnsupportedException("Encrypted RAR archives are not supported", "ERR_1002") if (rar.mainHeader.isSolid) throw MediaUnsupportedException("Solid RAR archives are not supported", "ERR_1003") @@ -32,7 +32,7 @@ class RarExtractor( try { val buffer = rar.getInputStream(entry).use { it.readBytes() } val mediaType = buffer.inputStream().use { contentDetector.detectMediaType(it) } - val dimension = if (contentDetector.isImage(mediaType)) + val dimension = if (analyzeDimensions && contentDetector.isImage(mediaType)) buffer.inputStream().use { imageAnalyzer.getDimension(it) } else null diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt index b84b412c4..33161b042 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt @@ -28,7 +28,7 @@ class ZipExtractor( override fun mediaTypes(): List = listOf("application/zip") - override fun getEntries(path: Path): List = + override fun getEntries(path: Path, analyzeDimensions: Boolean): List = ZipFile(path.toFile()).use { zip -> zip.entries.toList() .filter { !it.isDirectory } @@ -36,7 +36,7 @@ class ZipExtractor( try { zip.getInputStream(entry).buffered().use { stream -> val mediaType = contentDetector.detectMediaType(stream) - val dimension = if (contentDetector.isImage(mediaType)) + val dimension = if (analyzeDimensions && contentDetector.isImage(mediaType)) imageAnalyzer.getDimension(stream) else null diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt index bf6d0a04b..941a5afd5 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt @@ -92,6 +92,9 @@ class LibraryController( convertToCbz = library.convertToCbz, emptyTrashAfterScan = library.emptyTrashAfterScan, seriesCover = library.seriesCover.toDomain(), + hashFiles = library.hashFiles, + hashPages = library.hashPages, + analyzeDimensions = library.analyzeDimensions, ), ).toDto(includeRoot = principal.user.roleAdmin) } catch (e: Exception) { @@ -133,6 +136,9 @@ class LibraryController( convertToCbz = library.convertToCbz, emptyTrashAfterScan = library.emptyTrashAfterScan, seriesCover = library.seriesCover.toDomain(), + hashFiles = library.hashFiles, + hashPages = library.hashPages, + analyzeDimensions = library.analyzeDimensions, ) try { libraryLifecycle.updateLibrary(toUpdate) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryCreationDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryCreationDto.kt index d49e22cda..5a60ee10e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryCreationDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryCreationDto.kt @@ -20,4 +20,7 @@ data class LibraryCreationDto( val convertToCbz: Boolean = false, val emptyTrashAfterScan: Boolean = false, val seriesCover: SeriesCoverDto = SeriesCoverDto.FIRST, + val hashFiles: Boolean = true, + val hashPages: Boolean = false, + val analyzeDimensions: Boolean = true, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryDto.kt index a19d0b6a8..09157df74 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryDto.kt @@ -22,6 +22,9 @@ data class LibraryDto( val convertToCbz: Boolean, val emptyTrashAfterScan: Boolean, val seriesCover: SeriesCoverDto, + val hashFiles: Boolean, + val hashPages: Boolean, + val analyzeDimensions: Boolean, val unavailable: Boolean, ) @@ -44,5 +47,8 @@ fun Library.toDto(includeRoot: Boolean) = LibraryDto( convertToCbz = convertToCbz, emptyTrashAfterScan = emptyTrashAfterScan, seriesCover = seriesCover.toDto(), + hashFiles = hashFiles, + hashPages = hashPages, + analyzeDimensions = analyzeDimensions, unavailable = unavailableDate != null, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryUpdateDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryUpdateDto.kt index 5aabaee3d..f45cde464 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryUpdateDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/LibraryUpdateDto.kt @@ -20,4 +20,7 @@ data class LibraryUpdateDto( val convertToCbz: Boolean, val emptyTrashAfterScan: Boolean, val seriesCover: SeriesCoverDto, + val hashFiles: Boolean, + val hashPages: Boolean, + val analyzeDimensions: Boolean, ) diff --git a/komga/src/main/resources/application-dev.yml b/komga/src/main/resources/application-dev.yml index 7babe20da..f47c3e260 100644 --- a/komga/src/main/resources/application-dev.yml +++ b/komga/src/main/resources/application-dev.yml @@ -9,7 +9,6 @@ komga: file: ":memory:" cors.allowed-origins: - http://localhost:8081 -# file-hashing: false # delete-empty-collections: true # delete-empty-read-lists: true oauth2-account-creation: false diff --git a/komga/src/main/resources/application.yml b/komga/src/main/resources/application.yml index 4972cbd57..b60a59413 100644 --- a/komga/src/main/resources/application.yml +++ b/komga/src/main/resources/application.yml @@ -28,6 +28,8 @@ spring: enabled: true locations: classpath:db/migration/{vendor} mixed: true + placeholders: + library-file-hashing: \${komga.file-hashing:true} thymeleaf: prefix: classpath:/public/ mvc: diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt index 007ecb8aa..914b46cc9 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt @@ -2,6 +2,7 @@ package org.gotson.komga.domain.service import com.ninjasquad.springmockk.SpykBean import io.mockk.every +import io.mockk.verify import org.assertj.core.api.Assertions.assertThat import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookPage @@ -40,7 +41,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/rar4.rar") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/x-rar-compressed; version=4") assertThat(media.status).isEqualTo(Media.Status.READY) @@ -57,7 +58,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/rar4-solid.rar") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/x-rar-compressed; version=4") assertThat(media.status).isEqualTo(Media.Status.UNSUPPORTED) @@ -73,7 +74,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/$fileName") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/x-rar-compressed; version=5") assertThat(media.status).isEqualTo(Media.Status.UNSUPPORTED) @@ -89,7 +90,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/$fileName") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/x-7z-compressed") assertThat(media.status).isEqualTo(Media.Status.UNSUPPORTED) @@ -105,7 +106,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/$fileName") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/zip") assertThat(media.status).isEqualTo(Media.Status.READY) @@ -117,7 +118,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/zip-encrypted.zip") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/zip") assertThat(media.status).isEqualTo(Media.Status.ERROR) @@ -128,7 +129,7 @@ class BookAnalyzerTest( val file = ClassPathResource("archives/epub3.epub") val book = Book("book", file.url, LocalDateTime.now()) - val media = bookAnalyzer.analyze(book) + val media = bookAnalyzer.analyze(book, false) assertThat(media.mediaType).isEqualTo("application/epub+zip") assertThat(media.status).isEqualTo(Media.Status.READY) diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt index dc875b76f..b59bdf545 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt @@ -100,7 +100,7 @@ class BookLifecycleTest( assertThat(readProgressRepository.findAll()).hasSize(2) // when - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book.id) + every { mockAnalyzer.analyze(any(), any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book.id) bookLifecycle.analyzeAndPersist(book) // then @@ -133,7 +133,7 @@ class BookLifecycleTest( assertThat(readProgressRepository.findAll()).hasSize(2) // when - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = (1..10).map { BookPage("$it", "image/jpeg") }, bookId = book.id) + every { mockAnalyzer.analyze(any(), any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = (1..10).map { BookPage("$it", "image/jpeg") }, bookId = book.id) bookLifecycle.analyzeAndPersist(book) // then diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt index acc84a890..9cd37799a 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt @@ -284,7 +284,7 @@ class LibraryContentLifecycleTest( ) libraryContentLifecycle.scanRootFolder(library) - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id) + every { mockAnalyzer.analyze(any(), any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id) bookRepository.findAll().map { bookLifecycle.analyzeAndPersist(it) } // when @@ -292,7 +292,7 @@ class LibraryContentLifecycleTest( // then verify(exactly = 2) { mockScanner.scanRootFolder(any()) } - verify(exactly = 1) { mockAnalyzer.analyze(any()) } + verify(exactly = 1) { mockAnalyzer.analyze(any(), any()) } bookRepository.findAll().first().let { book -> assertThat(book.lastModifiedDate).isNotEqualTo(book.createdDate) @@ -320,7 +320,7 @@ class LibraryContentLifecycleTest( ) libraryContentLifecycle.scanRootFolder(library) - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id) + every { mockAnalyzer.analyze(any(), any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id) every { mockHasher.computeHash(any()) }.returnsMany("abc", "def") bookRepository.findAll().map { @@ -333,7 +333,7 @@ class LibraryContentLifecycleTest( // then verify(exactly = 2) { mockScanner.scanRootFolder(any()) } - verify(exactly = 1) { mockAnalyzer.analyze(any()) } + verify(exactly = 1) { mockAnalyzer.analyze(any(), any()) } verify(exactly = 1) { mockHasher.computeHash(any()) } bookRepository.findAll().first().let { book -> diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt index dc8b91377..cff44dc7a 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt @@ -68,6 +68,9 @@ class LibraryDaoTest( convertToCbz = true, emptyTrashAfterScan = true, seriesCover = Library.SeriesCover.LAST, + hashFiles = false, + hashPages = true, + analyzeDimensions = false, ) } @@ -95,6 +98,9 @@ class LibraryDaoTest( assertThat(modified.convertToCbz).isEqualTo(updated.convertToCbz) assertThat(modified.emptyTrashAfterScan).isEqualTo(updated.emptyTrashAfterScan) assertThat(modified.seriesCover).isEqualTo(updated.seriesCover) + assertThat(modified.hashFiles).isEqualTo(updated.hashFiles) + assertThat(modified.hashPages).isEqualTo(updated.hashPages) + assertThat(modified.analyzeDimensions).isEqualTo(updated.analyzeDimensions) } @Test diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractorTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractorTest.kt index 8b5a4714a..c05609362 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractorTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractorTest.kt @@ -19,7 +19,7 @@ class EpubExtractorTest { fun `given epub 3 file when parsing for entries then returns all images contained in pages`() { val epubResource = ClassPathResource("epub/The Incomplete Theft - Ralph Burke.epub") - val entries = epubExtractor.getEntries(epubResource.file.toPath()) + val entries = epubExtractor.getEntries(epubResource.file.toPath(), true) assertThat(entries).hasSize(1) with(entries.first()) { @@ -28,4 +28,18 @@ class EpubExtractorTest { assertThat(dimension).isEqualTo(Dimension(461, 616)) } } + + @Test + fun `given epub 3 file when parsing for entries without analyzing dimensions then returns all images contained in pages without dimensions`() { + val epubResource = ClassPathResource("epub/The Incomplete Theft - Ralph Burke.epub") + + val entries = epubExtractor.getEntries(epubResource.file.toPath(), false) + + assertThat(entries).hasSize(1) + with(entries.first()) { + assertThat(name).isEqualTo("cover.jpeg") + assertThat(mediaType).isEqualTo("image/jpeg") + assertThat(dimension).isNull() + } + } }