diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20220106143755__page_file_size.sql b/komga/src/flyway/resources/db/migration/sqlite/V20220106143755__page_file_size.sql new file mode 100644 index 000000000..20024299e --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20220106143755__page_file_size.sql @@ -0,0 +1,2 @@ +alter table media_page + add column FILE_SIZE int8 NULL; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt index b010844e8..dbd80229d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt @@ -5,4 +5,5 @@ data class BookPage( val mediaType: String, val dimension: Dimension? = null, val fileHash: String = "", + val fileSize: Long? = null, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt index 678f1a8de..294084e1f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt @@ -5,4 +5,5 @@ data class MediaContainerEntry( val mediaType: String? = null, val comment: String? = null, val dimension: Dimension? = null, + val fileSize: Long? = 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 e94b11369..82b6c52d6 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 @@ -60,7 +60,7 @@ class BookAnalyzer( entry.mediaType?.let { contentDetector.isImage(it) } ?: false }.let { (images, others) -> Pair( - images.map { BookPage(it.name, it.mediaType!!, it.dimension) }, + images.map { BookPage(fileName = it.name, mediaType = it.mediaType!!, dimension = it.dimension, fileSize = it.fileSize) }, others, ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt index 84d163269..b11b4cf8e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt @@ -131,7 +131,8 @@ class MediaDao( p.WIDTH, p.HEIGHT, p.FILE_HASH, - ).values(null as String?, null, null, null, null, null, null), + p.FILE_SIZE + ).values(null as String?, null, null, null, null, null, null, null), ).also { step -> chunk.forEach { media -> media.pages.forEachIndexed { index, page -> @@ -143,6 +144,7 @@ class MediaDao( page.dimension?.width, page.dimension?.height, page.fileHash, + page.fileSize, ) } } @@ -233,5 +235,6 @@ class MediaDao( mediaType = mediaType, dimension = if (width != null && height != null) Dimension(width, height) else null, fileHash = fileHash, + fileSize = fileSize ) } 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 264b8af7f..a10a9e549 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 @@ -1,6 +1,7 @@ package org.gotson.komga.infrastructure.mediacontainer import mu.KotlinLogging +import org.apache.commons.compress.archivers.ArchiveEntry import org.apache.commons.compress.archivers.zip.ZipFile import org.gotson.komga.domain.model.MediaContainerEntry import org.gotson.komga.domain.model.MediaUnsupportedException @@ -56,11 +57,13 @@ class EpubExtractor( val mediaType = manifest.values.first { it.href == (opfDir?.relativize(image) ?: image).invariantSeparatorsPathString }.mediaType + val zipEntry = zip.getEntry(name) val dimension = if (analyzeDimensions && contentDetector.isImage(mediaType)) - zip.getInputStream(zip.getEntry(name)).use { imageAnalyzer.getDimension(it) } + zip.getInputStream(zipEntry).use { imageAnalyzer.getDimension(it) } else null - MediaContainerEntry(name = name, mediaType = mediaType, dimension = dimension) + val fileSize = if (zipEntry.size == ArchiveEntry.SIZE_UNKNOWN) null else zipEntry.size + MediaContainerEntry(name = name, mediaType = mediaType, dimension = dimension, fileSize = fileSize) } } catch (e: Exception) { logger.error(e) { "File is not a proper Epub, treating it as a zip file" } 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 c5ad65427..5072155b8 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 @@ -8,7 +8,6 @@ import org.apache.pdfbox.rendering.ImageType import org.apache.pdfbox.rendering.PDFRenderer import org.gotson.komga.domain.model.Dimension import org.gotson.komga.domain.model.MediaContainerEntry -import org.gotson.komga.infrastructure.image.ImageAnalyzer import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream import java.nio.file.Path @@ -19,9 +18,7 @@ import kotlin.math.roundToInt private val logger = KotlinLogging.logger {} @Service -class PdfExtractor( - private val imageAnalyzer: ImageAnalyzer, -) : MediaContainerExtractor { +class PdfExtractor : MediaContainerExtractor { private val mediaType = "image/jpeg" private val imageIOFormat = "jpeg" 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 d230ed458..92b8e481d 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 @@ -36,7 +36,8 @@ class RarExtractor( buffer.inputStream().use { imageAnalyzer.getDimension(it) } else null - MediaContainerEntry(name = entry.fileName, mediaType = mediaType, dimension = dimension) + val fileSize = entry.fullUnpackSize + MediaContainerEntry(name = entry.fileName, mediaType = mediaType, dimension = dimension, fileSize = fileSize) } catch (e: Exception) { logger.warn(e) { "Could not analyze entry: ${entry.fileName}" } MediaContainerEntry(name = entry.fileName, comment = e.message) 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 33161b042..002429343 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 @@ -3,6 +3,7 @@ package org.gotson.komga.infrastructure.mediacontainer import com.github.benmanes.caffeine.cache.Caffeine import mu.KotlinLogging import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator +import org.apache.commons.compress.archivers.ArchiveEntry import org.apache.commons.compress.archivers.zip.ZipFile import org.gotson.komga.domain.model.MediaContainerEntry import org.gotson.komga.infrastructure.image.ImageAnalyzer @@ -40,7 +41,8 @@ class ZipExtractor( imageAnalyzer.getDimension(stream) else null - MediaContainerEntry(name = entry.name, mediaType = mediaType, dimension = dimension) + val fileSize = if (entry.size == ArchiveEntry.SIZE_UNKNOWN) null else entry.size + MediaContainerEntry(name = entry.name, mediaType = mediaType, dimension = dimension, fileSize = fileSize) } } catch (e: Exception) { logger.warn(e) { "Could not analyze entry: ${entry.name}" } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt index f084b883f..7bf43adfc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt @@ -432,8 +432,15 @@ class BookController( ) Media.Status.ERROR -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed") Media.Status.UNSUPPORTED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book format is not supported") - Media.Status.READY -> media.pages.mapIndexed { index, s -> - PageDto(index + 1, s.fileName, s.mediaType, s.dimension?.width, s.dimension?.height) + Media.Status.READY -> media.pages.mapIndexed { index, bookPage -> + PageDto( + number = index + 1, + fileName = bookPage.fileName, + mediaType = bookPage.mediaType, + width = bookPage.dimension?.width, + height = bookPage.dimension?.height, + sizeBytes = bookPage.fileSize, + ) } } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt index ec333adef..50dced5b6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt @@ -96,6 +96,7 @@ private fun BookWithMedia.toDto() = mediaType = bookPage.mediaType, width = bookPage.dimension?.width, height = bookPage.dimension?.height, + sizeBytes = bookPage.fileSize, ) }, files = media.files, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageDto.kt index a2ceb4b8b..856de0ccc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/PageDto.kt @@ -1,9 +1,13 @@ package org.gotson.komga.interfaces.api.rest.dto +import com.jakewharton.byteunits.BinaryByteUnit + data class PageDto( val number: Int, val fileName: String, val mediaType: String, val width: Int?, val height: Int?, + val sizeBytes: Long?, + val size: String = sizeBytes?.let { BinaryByteUnit.format(it) } ?: "", ) diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt index 654b2b6c8..c0446713f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt @@ -3,6 +3,7 @@ package org.gotson.komga.infrastructure.jooq import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.gotson.komga.domain.model.BookPage +import org.gotson.komga.domain.model.Dimension import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.model.makeLibrary @@ -68,6 +69,9 @@ class MediaDaoTest( BookPage( fileName = "1.jpg", mediaType = "image/jpeg", + dimension = Dimension(10, 10), + fileHash = "hashed", + fileSize = 10, ), ), files = listOf("ComicInfo.xml"), @@ -88,6 +92,9 @@ class MediaDaoTest( with(created.pages.first()) { assertThat(fileName).isEqualTo(media.pages.first().fileName) assertThat(mediaType).isEqualTo(media.pages.first().mediaType) + assertThat(dimension).isEqualTo(media.pages.first().dimension) + assertThat(fileHash).isEqualTo(media.pages.first().fileHash) + assertThat(fileSize).isEqualTo(media.pages.first().fileSize) } assertThat(created.files).hasSize(1) assertThat(created.files.first()).isEqualTo(media.files.first()) @@ -135,6 +142,9 @@ class MediaDaoTest( BookPage( fileName = "2.png", mediaType = "image/png", + dimension = Dimension(10, 10), + fileHash = "hashed", + fileSize = 10, ), ), files = listOf("id.txt"), @@ -155,6 +165,9 @@ class MediaDaoTest( assertThat(modified.comment).isEqualTo(updated.comment) assertThat(modified.pages.first().fileName).isEqualTo(updated.pages.first().fileName) assertThat(modified.pages.first().mediaType).isEqualTo(updated.pages.first().mediaType) + assertThat(modified.pages.first().dimension).isEqualTo(updated.pages.first().dimension) + assertThat(modified.pages.first().fileHash).isEqualTo(updated.pages.first().fileHash) + assertThat(modified.pages.first().fileSize).isEqualTo(updated.pages.first().fileSize) assertThat(modified.files.first()).isEqualTo(updated.files.first()) } 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 c05609362..d71fbd137 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 @@ -26,6 +26,7 @@ class EpubExtractorTest { assertThat(name).isEqualTo("cover.jpeg") assertThat(mediaType).isEqualTo("image/jpeg") assertThat(dimension).isEqualTo(Dimension(461, 616)) + assertThat(fileSize).isEqualTo(56756) } } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractorTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractorTest.kt new file mode 100644 index 000000000..6e643722d --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractorTest.kt @@ -0,0 +1,40 @@ +package org.gotson.komga.infrastructure.mediacontainer + +import org.assertj.core.api.Assertions +import org.gotson.komga.domain.model.Dimension +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource + +class PdfExtractorTest { + private val pdfExtractor = PdfExtractor() + + @Test + fun `given pdf file when parsing for entries then returns all images`() { + val fileResource = ClassPathResource("pdf/komga.pdf") + + val entries = pdfExtractor.getEntries(fileResource.file.toPath(), true) + + Assertions.assertThat(entries).hasSize(1) + with(entries.first()) { + Assertions.assertThat(name).isEqualTo("0") + Assertions.assertThat(mediaType).isEqualTo("image/jpeg") + Assertions.assertThat(dimension).isEqualTo(Dimension(1536, 1536)) + Assertions.assertThat(fileSize).isNull() + } + } + + @Test + fun `given pdf file when parsing for entries without analyzing dimensions then returns all images without dimensions`() { + val fileResource = ClassPathResource("pdf/komga.pdf") + + val entries = pdfExtractor.getEntries(fileResource.file.toPath(), false) + + Assertions.assertThat(entries).hasSize(1) + with(entries.first()) { + Assertions.assertThat(name).isEqualTo("0") + Assertions.assertThat(mediaType).isEqualTo("image/jpeg") + Assertions.assertThat(dimension).isNull() + Assertions.assertThat(fileSize).isNull() + } + } +} diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractorTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractorTest.kt new file mode 100644 index 000000000..caade7bed --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractorTest.kt @@ -0,0 +1,45 @@ +package org.gotson.komga.infrastructure.mediacontainer + +import org.apache.tika.config.TikaConfig +import org.assertj.core.api.Assertions +import org.gotson.komga.domain.model.Dimension +import org.gotson.komga.infrastructure.image.ImageAnalyzer +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource + +class RarExtractorTest { + + private val contentDetector = ContentDetector(TikaConfig()) + private val imageAnalyzer = ImageAnalyzer() + private val rarExtractor = RarExtractor(contentDetector, imageAnalyzer) + + @Test + fun `given rar file when parsing for entries then returns all images`() { + val fileResource = ClassPathResource("archives/rar4.rar") + + val entries = rarExtractor.getEntries(fileResource.file.toPath(), true) + + Assertions.assertThat(entries).hasSize(3) + with(entries.first()) { + Assertions.assertThat(name).isEqualTo("komga-1.png") + Assertions.assertThat(mediaType).isEqualTo("image/png") + Assertions.assertThat(dimension).isEqualTo(Dimension(48, 48)) + Assertions.assertThat(fileSize).isEqualTo(3108) + } + } + + @Test + fun `given rar file when parsing for entries without analyzing dimensions then returns all images without dimensions`() { + val fileResource = ClassPathResource("archives/rar4.rar") + + val entries = rarExtractor.getEntries(fileResource.file.toPath(), false) + + Assertions.assertThat(entries).hasSize(3) + with(entries.first()) { + Assertions.assertThat(name).isEqualTo("komga-1.png") + Assertions.assertThat(mediaType).isEqualTo("image/png") + Assertions.assertThat(dimension).isNull() + Assertions.assertThat(fileSize).isEqualTo(3108) + } + } +} diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractorTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractorTest.kt new file mode 100644 index 000000000..823ef7e52 --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractorTest.kt @@ -0,0 +1,45 @@ +package org.gotson.komga.infrastructure.mediacontainer + +import org.apache.tika.config.TikaConfig +import org.assertj.core.api.Assertions.assertThat +import org.gotson.komga.domain.model.Dimension +import org.gotson.komga.infrastructure.image.ImageAnalyzer +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource + +class ZipExtractorTest { + + private val contentDetector = ContentDetector(TikaConfig()) + private val imageAnalyzer = ImageAnalyzer() + private val zipExtractor = ZipExtractor(contentDetector, imageAnalyzer) + + @Test + fun `given zip file when parsing for entries then returns all images`() { + val fileResource = ClassPathResource("archives/zip.zip") + + val entries = zipExtractor.getEntries(fileResource.file.toPath(), true) + + assertThat(entries).hasSize(1) + with(entries.first()) { + assertThat(name).isEqualTo("komga.png") + assertThat(mediaType).isEqualTo("image/png") + assertThat(dimension).isEqualTo(Dimension(48, 48)) + assertThat(fileSize).isEqualTo(3108) + } + } + + @Test + fun `given zip file when parsing for entries without analyzing dimensions then returns all images without dimensions`() { + val fileResource = ClassPathResource("archives/zip.zip") + + val entries = zipExtractor.getEntries(fileResource.file.toPath(), false) + + assertThat(entries).hasSize(1) + with(entries.first()) { + assertThat(name).isEqualTo("komga.png") + assertThat(mediaType).isEqualTo("image/png") + assertThat(dimension).isNull() + assertThat(fileSize).isEqualTo(3108) + } + } +} diff --git a/komga/src/test/resources/pdf/komga.pdf b/komga/src/test/resources/pdf/komga.pdf new file mode 100644 index 000000000..c61ae3295 Binary files /dev/null and b/komga/src/test/resources/pdf/komga.pdf differ