Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor archive support with libarchive #949

Merged
merged 8 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, "") }
AntsyLich marked this conversation as resolved.
Show resolved Hide resolved
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