瀏覽代碼

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
arkon 1 年之前
父節點
當前提交
44619febd3

+ 1 - 1
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))
         }

+ 23 - 27
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<ReaderPage> {
-        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)

+ 36 - 24
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()
     }
 
-    override var isLocal: Boolean = true
-
-    override suspend fun getPages(): List<ReaderPage> {
-        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
+    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()
                 }
-            }
-            .toList()
+        }
     }
 
-    override suspend fun loadPage(page: ReaderPage) {
-        check(!isRecycled)
+    override var isLocal: Boolean = true
+
+    override suspend fun getPages(): List<ReaderPage> {
+        return DirectoryPageLoader(tmpDir).getPages()
     }
 
     override fun recycle() {
         super.recycle()
-        zip.close()
+        tmpDir.deleteRecursively()
     }
 }