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:
arkon 2023-04-23 11:59:58 -04:00 committed by GitHub
parent c48accb357
commit 44619febd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 60 additions and 52 deletions

View file

@ -78,7 +78,6 @@ class ChapterLoader(
val isDownloaded = downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source, skipCache = true) val isDownloaded = downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source, skipCache = true)
return when { return when {
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider) isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider)
source is HttpSource -> HttpPageLoader(chapter, source)
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
is Format.Directory -> DirectoryPageLoader(format.file) is Format.Directory -> DirectoryPageLoader(format.file)
@ -91,6 +90,7 @@ class ChapterLoader(
is Format.Epub -> EpubPageLoader(format.file) 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())) source is StubSource -> error(context.getString(R.string.source_not_installed, source.toString()))
else -> error(context.getString(R.string.loader_not_implemented_error)) else -> error(context.getString(R.string.loader_not_implemented_error))
} }

View file

@ -1,61 +1,57 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application
import com.github.junrar.Archive import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader 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.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import uy.kohesive.injekt.injectLazy
import tachiyomi.core.util.system.ImageUtil
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.PipedInputStream import java.io.PipedInputStream
import java.io.PipedOutputStream import java.io.PipedOutputStream
import java.util.concurrent.Executors
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
*/ */
internal class RarPageLoader(file: File) : PageLoader() { 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()
}
/** init {
* Pool for copying compressed files to an input stream. Archive(file).use { rar ->
*/ rar.fileHeaders.asSequence()
private val pool = Executors.newFixedThreadPool(1) .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 var isLocal: Boolean = true
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
return rar.fileHeaders.asSequence() return DirectoryPageLoader(tmpDir).getPages()
.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)
} }
override fun recycle() { override fun recycle() {
super.recycle() super.recycle()
rar.close() tmpDir.deleteRecursively()
pool.shutdown()
} }
/** /**
* Returns an input stream for the given [header]. * 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 pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn) val pipeOut = PipedOutputStream(pipeIn)
pool.execute { synchronized(this) {
try { try {
pipeOut.use { pipeOut.use {
rar.extractFile(header, it) rar.extractFile(header, it)

View file

@ -1,46 +1,58 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application
import android.os.Build import android.os.Build
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import uy.kohesive.injekt.injectLazy
import tachiyomi.core.util.system.ImageUtil
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets import java.io.FileInputStream
import java.util.zip.ZipFile import java.util.zip.ZipInputStream
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
internal class ZipPageLoader(file: File) : PageLoader() { internal class ZipPageLoader(file: File) : PageLoader() {
private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { private val context: Application by injectLazy()
ZipFile(file, StandardCharsets.ISO_8859_1) private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
} else { it.deleteRecursively()
ZipFile(file) 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 var isLocal: Boolean = true
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
return zip.entries().asSequence() return DirectoryPageLoader(tmpDir).getPages()
.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)
} }
override fun recycle() { override fun recycle() {
super.recycle() super.recycle()
zip.close() tmpDir.deleteRecursively()
} }
} }