mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Refactor archive support with libarchive (#949)
* Refactor archive support with libarchive * Revert string resource changs * Only mark archive formats as supported Comic book archives should not be compressed. * Fixup * Remove epub from archive format list * Move to mihon package * Format * Cleanup
This commit is contained in:
@ -203,7 +203,6 @@ dependencies {
|
||||
// Disk
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.bundles.archive)
|
||||
|
||||
// Preferences
|
||||
implementation(libs.preferencektx)
|
||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -77,9 +77,6 @@
|
||||
# XmlUtil
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||
|
||||
# Apache Commons Compress
|
||||
-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { <init>(); }
|
||||
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import logcat.LogPriority
|
||||
import mihon.core.common.archive.ZipWriter
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@ -58,12 +59,8 @@ import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
@ -594,25 +591,9 @@ class Downloader(
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
|
||||
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
|
||||
zipOut.setMethod(ZipEntry.STORED)
|
||||
|
||||
tmpDir.listFiles()?.forEach { img ->
|
||||
img.openInputStream().use { input ->
|
||||
val data = input.readBytes()
|
||||
val size = img.length()
|
||||
val entry = ZipEntry(img.name).apply {
|
||||
val crc = CRC32().apply {
|
||||
update(data)
|
||||
}
|
||||
setCrc(crc.value)
|
||||
|
||||
compressedSize = size
|
||||
setSize(size)
|
||||
}
|
||||
zipOut.putNextEntry(entry)
|
||||
zipOut.write(data)
|
||||
}
|
||||
ZipWriter(context, zip).use { writer ->
|
||||
tmpDir.listFiles()?.forEach { file ->
|
||||
writer.write(file)
|
||||
}
|
||||
}
|
||||
zip.renameTo("$dirname.cbz")
|
||||
|
@ -3,26 +3,22 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import mihon.core.common.extensions.toZipFile
|
||||
import mihon.core.common.archive.ArchiveReader
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
* Loader used to load a chapter from an archive file.
|
||||
*/
|
||||
internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() {
|
||||
|
||||
private val zip = channel.toZipFile()
|
||||
|
||||
internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() {
|
||||
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) } }
|
||||
override suspend fun getPages(): List<ReaderPage> = reader.useEntries { entries ->
|
||||
entries
|
||||
.filter { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { zip.getInputStream(entry) }
|
||||
stream = { reader.getInputStream(entry.name)!! }
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
@ -35,6 +31,6 @@ internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() {
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
zip.close()
|
||||
reader.close()
|
||||
}
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.exception.UnsupportedRarV5Exception
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import mihon.core.common.archive.archiveReader
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.openReadOnlyChannel
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
@ -95,13 +94,8 @@ class ChapterLoader(
|
||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||
is Format.Zip -> ZipPageLoader(format.file.openReadOnlyChannel(context))
|
||||
is Format.Rar -> try {
|
||||
RarPageLoader(format.file.openInputStream())
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
error(context.stringResource(MR.strings.loader_rar5_error))
|
||||
}
|
||||
is Format.Epub -> EpubPageLoader(format.file.openReadOnlyChannel(context))
|
||||
is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context))
|
||||
is Format.Epub -> EpubPageLoader(format.file.archiveReader(context))
|
||||
}
|
||||
}
|
||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
|
@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import tachiyomi.core.common.storage.openReadOnlyChannel
|
||||
import mihon.core.common.archive.archiveReader
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@ -27,7 +27,7 @@ internal class DownloadPageLoader(
|
||||
|
||||
private val context: Application by injectLazy()
|
||||
|
||||
private var zipPageLoader: ZipPageLoader? = null
|
||||
private var archivePageLoader: ArchivePageLoader? = null
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
@ -43,11 +43,11 @@ internal class DownloadPageLoader(
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
zipPageLoader?.recycle()
|
||||
archivePageLoader?.recycle()
|
||||
}
|
||||
|
||||
private suspend fun getPagesFromArchive(file: UniFile): List<ReaderPage> {
|
||||
val loader = ZipPageLoader(file.openReadOnlyChannel(context)).also { zipPageLoader = it }
|
||||
val loader = ArchivePageLoader(file.archiveReader(context)).also { archivePageLoader = it }
|
||||
return loader.getPages()
|
||||
}
|
||||
|
||||
@ -63,6 +63,6 @@ internal class DownloadPageLoader(
|
||||
}
|
||||
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
zipPageLoader?.loadPage(page)
|
||||
archivePageLoader?.loadPage(page)
|
||||
}
|
||||
}
|
||||
|
@ -3,21 +3,21 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
import mihon.core.common.archive.ArchiveReader
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .epub file.
|
||||
*/
|
||||
internal class EpubPageLoader(channel: SeekableByteChannel) : PageLoader() {
|
||||
internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() {
|
||||
|
||||
private val epub = EpubFile(channel)
|
||||
private val epub = EpubFile(reader)
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return epub.getImagesFromPages()
|
||||
.mapIndexed { i, path ->
|
||||
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
|
||||
val streamFn = { epub.getInputStream(path)!! }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.State.READY
|
||||
|
@ -1,67 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
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.common.util.system.ImageUtil
|
||||
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(inputStream: InputStream) : PageLoader() {
|
||||
|
||||
private val rar = Archive(inputStream)
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
/**
|
||||
* Pool for copying compressed files to an input stream.
|
||||
*/
|
||||
private val pool = Executors.newFixedThreadPool(1)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
rar.close()
|
||||
pool.shutdown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream for the given [header].
|
||||
*/
|
||||
private fun getStream(header: FileHeader): InputStream {
|
||||
val pipeIn = PipedInputStream()
|
||||
val pipeOut = PipedOutputStream(pipeIn)
|
||||
pool.execute {
|
||||
try {
|
||||
pipeOut.use {
|
||||
rar.extractFile(header, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
return pipeIn
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user