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:
parent
c48accb357
commit
44619febd3
3 changed files with 60 additions and 52 deletions
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
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 {
|
} else {
|
||||||
ZipFile(file)
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue