diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt index f6709f66..6de0382d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample import libcore.net.MimeUtils import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update @@ -23,6 +22,7 @@ import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.util.ConversionUtil import java.io.File @@ -30,6 +30,7 @@ import java.io.InputStream import javax.imageio.IIOImage import javax.imageio.ImageIO import javax.imageio.ImageWriteParam +import javax.imageio.ImageWriter sealed class FileType { data class RegularFile( @@ -175,7 +176,7 @@ abstract class ChaptersFilesProvider( }, ) - maybeConvertChapterImages(downloadCacheFolder) + maybeConvertPages(downloadCacheFolder) handleSuccessfulDownload() @@ -197,63 +198,103 @@ abstract class ChaptersFilesProvider( abstract fun getAsArchiveStream(): Pair - private fun maybeConvertChapterImages(chapterCacheFolder: File) { - if (chapterCacheFolder.isDirectory) { - val conv = serverConfig.downloadConversions.value + private fun maybeConvertPages(chapterCacheFolder: File) { + val conversions = serverConfig.downloadConversions.value + + if (!chapterCacheFolder.isDirectory || conversions.isEmpty()) { + return + } + + val pages = chapterCacheFolder .listFiles() .orEmpty() .filter { it.name != COMIC_INFO_FILE } - .forEach { - val imageType = MimeUtils.guessMimeTypeFromExtension(it.extension) ?: return@forEach - val targetConversion = - conv.getOrElse(imageType) { - conv.getOrElse("default") { - logger.debug { "Skipping conversion of $it since no conversion specified" } - return@forEach - } - } - val targetMime = targetConversion.target - if (imageType == targetMime || targetMime == "none") return@forEach // nothing to do - logger.debug { "Converting $it to $targetMime" } - val targetExtension = MimeUtils.guessExtensionFromMimeType(targetMime) ?: targetMime.removePrefix("image/") - val outFile = File(it.parentFile, it.nameWithoutExtension + "." + targetExtension) + val pagesByMimeType = + pages + .groupBy { MimeUtils.guessMimeTypeFromExtension(it.extension) } + .mapValues { it.value.map { it.nameWithoutExtension } } - val writers = ImageIO.getImageWritersByMIMEType(targetMime) - val writer = - try { - writers.next() - } catch (_: NoSuchElementException) { - logger.warn { "Conversion aborted: No reader for target format $targetMime" } - return@forEach - } - val writerParams = writer.defaultWriteParam - targetConversion.compressionLevel?.let { - writerParams.compressionMode = ImageWriteParam.MODE_EXPLICIT - writerParams.compressionQuality = it.toFloat() - } - val success = - try { - ImageIO.createImageOutputStream(outFile).use { outStream -> - writer.setOutput(outStream) + logger.debug { "maybeConvertPages: pagesByMimeType= $pagesByMimeType; conversions= $conversions" } - val inImage = ConversionUtil.readImage(it) ?: return@use false - writer.write(null, IIOImage(inImage, null, null), writerParams) + pages.forEach { page -> + val imageType = MimeUtils.guessMimeTypeFromExtension(page.extension) ?: return@forEach - true - } - } catch (e: Exception) { - logger.warn(e) { "Conversion aborted: for image $it" } - false - } - writer.dispose() - if (success) { - it.delete() - } else { - outFile.delete() - } - } + val defaultConversion = conversions["default"] + val conversion = conversions[imageType] + val targetConversion = conversion ?: defaultConversion ?: return@forEach + + val (targetMime) = targetConversion + val requiresConversion = imageType != targetMime && targetMime != "none" + if (!requiresConversion) { + return@forEach + } + + convertPage(page, targetConversion) } } + + private fun convertPage( + page: File, + conversion: ServerConfig.DownloadConversion, + ) { + val (targetMime, compressionLevel) = conversion + + val targetExtension = + MimeUtils.guessExtensionFromMimeType(targetMime) ?: targetMime.removePrefix("image/") + + val convertedPage = File(page.parentFile, page.nameWithoutExtension + "." + targetExtension) + + val conversionWriter = getConversionWriter(targetMime, compressionLevel) + if (conversionWriter == null) { + logger.warn { "Conversion aborted: No reader for target format $targetMime" } + return + } + + val (writer, writerParams) = conversionWriter + + val success = + try { + ImageIO.createImageOutputStream(convertedPage).use { outStream -> + writer.setOutput(outStream) + + val inImage = ConversionUtil.readImage(page) ?: return@use false + writer.write(null, IIOImage(inImage, null, null), writerParams) + + true + } + } catch (e: Exception) { + logger.warn(e) { "Conversion aborted: for image $page" } + false + } + writer.dispose() + + if (success) { + page.delete() + } else { + convertedPage.delete() + } + } + + private fun getConversionWriter( + targetMime: String, + compressionLevel: Double?, + ): Pair? { + val writers = ImageIO.getImageWritersByMIMEType(targetMime) + val writer = + try { + writers.next() + } catch (_: NoSuchElementException) { + return null + } + + val writerParams = writer.defaultWriteParam + compressionLevel?.let { + writerParams.compressionMode = ImageWriteParam.MODE_EXPLICIT + writerParams.compressionQuality = it.toFloat() + } + + return writer to writerParams + } }