From 4c1301f45eb7dc95bc0ff91c444224fb3268a91c Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Thu, 4 Jan 2024 17:59:15 +0800 Subject: [PATCH] feat(komga): support for RAR5 via libarchive --- komga/build.gradle.kts | 1 + .../gotson/komga/domain/model/MediaType.kt | 1 + .../komga/domain/service/BookAnalyzer.kt | 3 +- .../komga/domain/service/BookConverter.kt | 4 +- .../mediacontainer/divina/Rar5Extractor.kt | 70 +++++++++++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/divina/Rar5Extractor.kt diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index bdaa68598..e401bcfa4 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -89,6 +89,7 @@ dependencies { implementation("org.apache.tika:tika-core:2.9.1") implementation("org.apache.commons:commons-compress:1.24.0") implementation("com.github.junrar:junrar:7.5.5") + implementation("com.github.gotson.nightcompress:nightcompress:0.2.0") implementation("org.apache.pdfbox:pdfbox:2.0.28") implementation("net.grey-panther:natural-comparator:1.1") implementation("org.jsoup:jsoup:1.16.2") diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaType.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaType.kt index 0a6fe37d5..592e77a76 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaType.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaType.kt @@ -4,6 +4,7 @@ enum class MediaType(val type: String, val profile: MediaProfile, val fileExtens ZIP("application/zip", MediaProfile.DIVINA, "cbz", "application/vnd.comicbook+zip"), RAR_GENERIC("application/x-rar-compressed", MediaProfile.DIVINA, "cbr", "application/vnd.comicbook-rar"), RAR_4("application/x-rar-compressed; version=4", MediaProfile.DIVINA, "cbr", "application/vnd.comicbook-rar"), + RAR_5("application/x-rar-compressed; version=5", MediaProfile.DIVINA, "cbr", "application/vnd.comicbook-rar"), EPUB("application/epub+zip", MediaProfile.EPUB, "epub"), PDF("application/pdf", MediaProfile.PDF, "pdf"), ; 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 52ca4cc4f..e3c9022b9 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 @@ -91,7 +91,8 @@ class BookAnalyzer( private fun analyzeDivina(book: Book, mediaType: MediaType, analyzeDimensions: Boolean): Media { val entries = try { - divinaExtractors.getValue(mediaType.type).getEntries(book.path, analyzeDimensions) + divinaExtractors[mediaType.type]?.getEntries(book.path, analyzeDimensions) + ?: return Media(status = Media.Status.UNSUPPORTED) } catch (ex: MediaUnsupportedException) { return Media(status = Media.Status.UNSUPPORTED, comment = ex.code) } 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 ad3ee8e36..af23b77f7 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 @@ -50,10 +50,10 @@ class BookConverter( private val historicalEventRepository: HistoricalEventRepository, ) { - private val convertibleTypes = listOf(MediaType.RAR_4.type) + private val convertibleTypes = listOf(MediaType.RAR_4.type, MediaType.RAR_5.type) private val mediaTypeToExtension = - listOf(MediaType.RAR_4, MediaType.ZIP, MediaType.PDF, MediaType.EPUB) + listOf(MediaType.RAR_4, MediaType.RAR_5, MediaType.ZIP, MediaType.PDF, MediaType.EPUB) .associate { it.type to it.fileExtension } private val failedConversions = mutableListOf() diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/divina/Rar5Extractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/divina/Rar5Extractor.kt new file mode 100644 index 000000000..e5c98d324 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/divina/Rar5Extractor.kt @@ -0,0 +1,70 @@ +package org.gotson.komga.infrastructure.mediacontainer.divina + +import com.github.gotson.nightcompress.Archive +import mu.KotlinLogging +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator +import org.gotson.komga.domain.model.MediaContainerEntry +import org.gotson.komga.domain.model.MediaType +import org.gotson.komga.infrastructure.image.ImageAnalyzer +import org.gotson.komga.infrastructure.mediacontainer.ContentDetector +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor +import org.springframework.context.annotation.Configuration +import java.nio.file.Path + + +private val logger = KotlinLogging.logger {} + +@Configuration +class Rar5Configuration : BeanDefinitionRegistryPostProcessor { + override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {} + + override fun postProcessBeanDefinitionRegistry(registry: BeanDefinitionRegistry) { + try { + if (Archive.isAvailable()) { + val builder = BeanDefinitionBuilder.genericBeanDefinition(Rar5Extractor::class.java).setLazyInit(true) + registry.registerBeanDefinition("rar5Extractor", builder.beanDefinition) + logger.info { "Rar5 extractor is enabled" } + } + } catch (e: Throwable) { + logger.warn(e) { "Rar5 extractor count not be loaded" } + } + } +} + +class Rar5Extractor( + private val contentDetector: ContentDetector, + private val imageAnalyzer: ImageAnalyzer, +) : DivinaExtractor { + + private val natSortComparator: Comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() + + override fun mediaTypes(): List = listOf(MediaType.RAR_5.type) + + override fun getEntries(path: Path, analyzeDimensions: Boolean): List = + Archive(path).use { rar -> + generateSequence { rar.nextEntry } + .map { entry -> + try { + val buffer = rar.inputStream.use { it.readBytes() } + val mediaType = buffer.inputStream().use { contentDetector.detectMediaType(it) } + val dimension = if (analyzeDimensions && contentDetector.isImage(mediaType)) + buffer.inputStream().use { imageAnalyzer.getDimension(it) } + else + null + val fileSize = entry.size + MediaContainerEntry(name = entry.name, mediaType = mediaType, dimension = dimension, fileSize = fileSize) + } catch (e: Exception) { + logger.warn(e) { "Could not analyze entry: ${entry.name}" } + MediaContainerEntry(name = entry.name, comment = e.message) + } + } + .sortedWith(compareBy(natSortComparator) { it.name }) + .toList() + } + + override fun getEntryStream(path: Path, entryName: String): ByteArray = + Archive.getInputStream(path, entryName).use { it?.readBytes() ?: ByteArray(0) } +}