From 44619febd333f4e662cdbf149ae0741a43ebd27b Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 23 Apr 2023 11:59:58 -0400 Subject: [PATCH] Load ZIP file contents to cache (#9381) * Extract downloaded archives to tmp folder when loading for viewing * Generate sequence of entries from ZipInputStream instead of loading entire ZipFile --- .../ui/reader/loader/ChapterLoader.kt | 2 +- .../ui/reader/loader/RarPageLoader.kt | 50 +++++++--------- .../ui/reader/loader/ZipPageLoader.kt | 60 +++++++++++-------- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index cf62e23712..d015a29b7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -78,7 +78,6 @@ class ChapterLoader( val isDownloaded = downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source, skipCache = true) return when { isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider) - source is HttpSource -> HttpPageLoader(chapter, source) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) @@ -91,6 +90,7 @@ class ChapterLoader( is Format.Epub -> EpubPageLoader(format.file) } } + source is HttpSource -> HttpPageLoader(chapter, source) source is StubSource -> error(context.getString(R.string.source_not_installed, source.toString())) else -> error(context.getString(R.string.loader_not_implemented_error)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 5374216eee..724283e566 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -1,61 +1,57 @@ package eu.kanade.tachiyomi.ui.reader.loader +import android.app.Application import com.github.junrar.Archive import com.github.junrar.rarfile.FileHeader -import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import tachiyomi.core.util.system.ImageUtil +import uy.kohesive.injekt.injectLazy import java.io.File import java.io.InputStream import java.io.PipedInputStream import java.io.PipedOutputStream -import java.util.concurrent.Executors /** * Loader used to load a chapter from a .rar or .cbr file. */ internal class RarPageLoader(file: File) : PageLoader() { - private val rar = Archive(file) + private val context: Application by injectLazy() + private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { + it.deleteRecursively() + it.mkdirs() + } - /** - * Pool for copying compressed files to an input stream. - */ - private val pool = Executors.newFixedThreadPool(1) + init { + Archive(file).use { rar -> + rar.fileHeaders.asSequence() + .filterNot { it.isDirectory } + .forEach { header -> + val pageFile = File(tmpDir, header.fileName).also { it.createNewFile() } + getStream(rar, header).use { + it.copyTo(pageFile.outputStream()) + } + } + } + } override var isLocal: Boolean = true override suspend fun getPages(): List { - return rar.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .mapIndexed { i, header -> - ReaderPage(i).apply { - stream = { getStream(header) } - status = Page.State.READY - } - } - .toList() - } - - override suspend fun loadPage(page: ReaderPage) { - check(!isRecycled) + return DirectoryPageLoader(tmpDir).getPages() } override fun recycle() { super.recycle() - rar.close() - pool.shutdown() + tmpDir.deleteRecursively() } /** * Returns an input stream for the given [header]. */ - private fun getStream(header: FileHeader): InputStream { + private fun getStream(rar: Archive, header: FileHeader): InputStream { val pipeIn = PipedInputStream() val pipeOut = PipedOutputStream(pipeIn) - pool.execute { + synchronized(this) { try { pipeOut.use { rar.extractFile(header, it) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index e04fe78e6a..8ebe393be0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -1,46 +1,58 @@ package eu.kanade.tachiyomi.ui.reader.loader +import android.app.Application import android.os.Build -import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import tachiyomi.core.util.system.ImageUtil +import uy.kohesive.injekt.injectLazy import java.io.File -import java.nio.charset.StandardCharsets -import java.util.zip.ZipFile +import java.io.FileInputStream +import java.util.zip.ZipInputStream /** * Loader used to load a chapter from a .zip or .cbz file. */ internal class ZipPageLoader(file: File) : PageLoader() { - private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ZipFile(file, StandardCharsets.ISO_8859_1) - } else { - ZipFile(file) + private val context: Application by injectLazy() + private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { + it.deleteRecursively() + it.mkdirs() + } + + init { + ZipInputStream(FileInputStream(file)).use { zipInputStream -> + generateSequence { zipInputStream.nextEntry } + .filterNot { it.isDirectory } + .forEach { entry -> + File(tmpDir, entry.name).also { it.createNewFile() } + .outputStream().use { pageOutputStream -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pageOutputStream.write(zipInputStream.readNBytes(entry.size.toInt())) + } else { + val buffer = ByteArray(2048) + var len: Int + while ( + zipInputStream.read(buffer, 0, buffer.size) + .also { len = it } >= 0 + ) { + pageOutputStream.write(buffer, 0, len) + } + } + pageOutputStream.flush() + } + zipInputStream.closeEntry() + } + } } override var isLocal: Boolean = true override suspend fun getPages(): List { - return zip.entries().asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .mapIndexed { i, entry -> - ReaderPage(i).apply { - stream = { zip.getInputStream(entry) } - status = Page.State.READY - } - } - .toList() - } - - override suspend fun loadPage(page: ReaderPage) { - check(!isRecycled) + return DirectoryPageLoader(tmpDir).getPages() } override fun recycle() { super.recycle() - zip.close() + tmpDir.deleteRecursively() } }