Skip to content

Commit

Permalink
Refactor archive support with libarchive (#949)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
FooIbar authored Jun 26, 2024
1 parent 36e40c0 commit 239c389
Show file tree
Hide file tree
Showing 22 changed files with 239 additions and 233 deletions.
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ dependencies {
// Disk
implementation(libs.disklrucache)
implementation(libs.unifile)
implementation(libs.bundles.archive)

// Preferences
implementation(libs.preferencektx)
Expand Down
3 changes: 0 additions & 3 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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.** { *; }
27 changes: 4 additions & 23 deletions app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -35,6 +31,6 @@ internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() {

override fun recycle() {
super.recycle()
zip.close()
reader.close()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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()
}

Expand All @@ -63,6 +63,6 @@ internal class DownloadPageLoader(
}

override suspend fun loadPage(page: ReaderPage) {
zipPageLoader?.loadPage(page)
archivePageLoader?.loadPage(page)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies {
implementation(libs.image.decoder)

implementation(libs.unifile)
implementation(libs.bundles.archive)
implementation(libs.libarchive)

api(kotlinx.coroutines.core)
api(kotlinx.serialization.json)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,27 @@
package eu.kanade.tachiyomi.util.storage

import mihon.core.common.extensions.toZipFile
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import mihon.core.common.archive.ArchiveReader
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.nio.channels.SeekableByteChannel

/**
* Wrapper over ZipFile to load files in epub format.
*/
class EpubFile(channel: SeekableByteChannel) : Closeable {

/**
* Zip file of this epub.
*/
private val zip = channel.toZipFile()
class EpubFile(private val reader: ArchiveReader) : Closeable by reader {

/**
* Path separator used by this epub.
*/
private val pathSeparator = getPathSeparator()

/**
* Closes the underlying zip file.
*/
override fun close() {
zip.close()
}

/**
* Returns an input stream for reading the contents of the specified zip file entry.
*/
fun getInputStream(entry: ZipArchiveEntry): InputStream {
return zip.getInputStream(entry)
}

/**
* Returns the zip file entry for the specified name, or null if not found.
*/
fun getEntry(name: String): ZipArchiveEntry? {
return zip.getEntry(name)
fun getInputStream(entryName: String): InputStream? {
return reader.getInputStream(entryName)
}

/**
Expand All @@ -59,9 +38,9 @@ class EpubFile(channel: SeekableByteChannel) : Closeable {
* Returns the path to the package document.
*/
fun getPackageHref(): String {
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
val meta = getInputStream(resolveZipPath("META-INF", "container.xml"))
if (meta != null) {
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
val metaDoc = meta.use { Jsoup.parse(it, null, "") }
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
if (path != null) {
return path
Expand All @@ -74,8 +53,7 @@ class EpubFile(channel: SeekableByteChannel) : Closeable {
* Returns the package document where all the files are listed.
*/
fun getPackageDocument(ref: String): Document {
val entry = zip.getEntry(ref)
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
return getInputStream(ref)!!.use { Jsoup.parse(it, null, "") }
}

/**
Expand All @@ -98,8 +76,7 @@ class EpubFile(channel: SeekableByteChannel) : Closeable {
val basePath = getParentDirectory(packageHref)
pages.forEach { page ->
val entryPath = resolveZipPath(basePath, page)
val entry = zip.getEntry(entryPath)
val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
val document = getInputStream(entryPath)!!.use { Jsoup.parse(it, null, "") }
val imageBasePath = getParentDirectory(entryPath)

document.allElements.forEach {
Expand All @@ -117,8 +94,9 @@ class EpubFile(channel: SeekableByteChannel) : Closeable {
* Returns the path separator used by the epub file.
*/
private fun getPathSeparator(): String {
val meta = zip.getEntry("META-INF\\container.xml")
val meta = getInputStream("META-INF\\container.xml")
return if (meta != null) {
meta.close()
"\\"
} else {
"/"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package mihon.core.common.archive

class ArchiveEntry(
val name: String,
val isFile: Boolean,
)
Loading

0 comments on commit 239c389

Please sign in to comment.