mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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
This commit is contained in:
		@@ -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))
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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<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
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .toList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun loadPage(page: ReaderPage) {
 | 
			
		||||
        check(!isRecycled)
 | 
			
		||||
        return DirectoryPageLoader(tmpDir).getPages()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun recycle() {
 | 
			
		||||
        super.recycle()
 | 
			
		||||
        zip.close()
 | 
			
		||||
        tmpDir.deleteRecursively()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user