Libarchive refactor (#1249)
* Refactor archive support with libarchive * 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 Co-authored-by: Shamicen <84282253+Shamicen@users.noreply.github.com> (cherry picked from commit 239c38982c4fd55d4d86b37fd9c3c51c3b47d098) * handle incorrect passwords * lint * fixed broken encryption detection + small tweaks * Add safeguard to prevent ArchiveInputStream from being closed twice (#967) * fix: Add safeguard to prevent ArchiveInputStream from being closed twice * detekt * lint: Make detekt happy --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit e620665dda9eb5cc39f09e6087ea4f60a3cbe150) * fixed ArchiveReaderMode CACHE_TO_DISK * Added some missing SY --> comments --------- Co-authored-by: FooIbar <118464521+fooibar@users.noreply.github.com> Co-authored-by: Ahmad Ansori Palembani <46041660+null2264@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package tachiyomi.source.local
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
@@ -12,22 +11,18 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import logcat.LogPriority
|
||||
import mihon.core.common.archive.ZipWriter
|
||||
import mihon.core.common.archive.archiveReader
|
||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.UniFileTempFileManager
|
||||
import tachiyomi.core.common.storage.addStreamToZip
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.storage.getCoverStreamFromZip
|
||||
import tachiyomi.core.common.storage.getZipInputStream
|
||||
import tachiyomi.core.common.storage.isEncryptedZip
|
||||
import tachiyomi.core.common.storage.nameWithoutExtension
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
@@ -51,7 +46,6 @@ import uy.kohesive.injekt.injectLazy
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import com.github.junrar.Archive as JunrarArchive
|
||||
import tachiyomi.domain.source.model.Source as DomainSource
|
||||
|
||||
actual class LocalSource(
|
||||
@@ -65,7 +59,6 @@ actual class LocalSource(
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
private val tempFileManager: UniFileTempFileManager by injectLazy()
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy.Latest(context))
|
||||
@@ -161,13 +154,14 @@ actual class LocalSource(
|
||||
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url)
|
||||
val existingFile = mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
val comicInfoArchiveFile = mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_ARCHIVE }
|
||||
val existingComicInfo = (existingFile?.openInputStream() ?: comicInfoArchiveFile?.getZipInputStream(COMIC_INFO_FILE))?.use {
|
||||
AndroidXmlReader(it, StandardCharsets.UTF_8.name()).use {
|
||||
xml.decodeFromReader<ComicInfo>(it)
|
||||
val comicInfoArchiveFile = mangaDirFiles.firstOrNull { it.name == COMIC_INFO_ARCHIVE }
|
||||
val comicInfoArchiveReader = comicInfoArchiveFile?.archiveReader(context)
|
||||
val existingComicInfo =
|
||||
(existingFile?.openInputStream() ?: comicInfoArchiveReader?.getInputStream(COMIC_INFO_FILE))?.use {
|
||||
AndroidXmlReader(it, StandardCharsets.UTF_8.name()).use { xmlReader ->
|
||||
xml.decodeFromReader<ComicInfo>(xmlReader)
|
||||
}
|
||||
}
|
||||
}
|
||||
val newComicInfo = if (existingComicInfo != null) {
|
||||
manga.run {
|
||||
existingComicInfo.copy(
|
||||
@@ -188,8 +182,9 @@ actual class LocalSource(
|
||||
fileSystem.getMangaDirectory(manga.url)?.let {
|
||||
copyComicInfoFile(
|
||||
xml.encodeToString(ComicInfo.serializer(), newComicInfo).byteInputStream(),
|
||||
it
|
||||
)
|
||||
it,
|
||||
comicInfoArchiveReader?.encrypted ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
@@ -202,8 +197,8 @@ actual class LocalSource(
|
||||
|
||||
// Augment manga details based on metadata files
|
||||
try {
|
||||
val mangaDir by lazy { fileSystem.getMangaDirectory(manga.url) }
|
||||
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url)
|
||||
val mangaDir = fileSystem.getMangaDirectory(manga.url) ?: error("${manga.url} is not a valid directory")
|
||||
val mangaDirFiles = mangaDir.listFiles().orEmpty()
|
||||
|
||||
val comicInfoFile = mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
@@ -226,7 +221,7 @@ actual class LocalSource(
|
||||
comicInfoArchiveFile != null -> {
|
||||
noXmlFile?.delete()
|
||||
|
||||
comicInfoArchiveFile.getZipInputStream(COMIC_INFO_FILE)
|
||||
comicInfoArchiveFile.archiveReader(context).getInputStream(COMIC_INFO_FILE)
|
||||
?.let { setMangaDetailsFromComicInfoFile(it, manga) }
|
||||
}
|
||||
|
||||
@@ -246,7 +241,7 @@ actual class LocalSource(
|
||||
// Replace with ComicInfo.xml file
|
||||
val comicInfo = manga.getComicInfo()
|
||||
mangaDir
|
||||
?.createFile(COMIC_INFO_FILE)
|
||||
.createFile(COMIC_INFO_FILE)
|
||||
?.openOutputStream()
|
||||
?.use {
|
||||
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
|
||||
@@ -257,21 +252,20 @@ actual class LocalSource(
|
||||
|
||||
// Copy ComicInfo.xml from chapter archive to top level if found
|
||||
noXmlFile == null -> {
|
||||
val chapterArchives = mangaDirFiles
|
||||
.filter(Archive::isSupported)
|
||||
.toList()
|
||||
val chapterArchives = mangaDirFiles.filter(Archive::isSupported)
|
||||
|
||||
val copiedFile = mangaDir?.let { copyComicInfoFileFromArchive(chapterArchives, it) }
|
||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, mangaDir)
|
||||
|
||||
// SY -->
|
||||
if (copiedFile != null && copiedFile.name != COMIC_INFO_ARCHIVE) {
|
||||
setMangaDetailsFromComicInfoFile(copiedFile.openInputStream(), manga)
|
||||
} else if (copiedFile != null && copiedFile.name == COMIC_INFO_ARCHIVE) {
|
||||
copiedFile.getZipInputStream(COMIC_INFO_FILE)?.let { setMangaDetailsFromComicInfoFile(it, manga) }
|
||||
copiedFile.archiveReader(context).getInputStream(COMIC_INFO_FILE)
|
||||
?.let { setMangaDetailsFromComicInfoFile(it, manga) }
|
||||
} // SY <--
|
||||
else {
|
||||
// Avoid re-scanning
|
||||
mangaDir?.createFile(".noxml")
|
||||
mangaDir.createFile(".noxml")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,44 +278,31 @@ actual class LocalSource(
|
||||
|
||||
private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folder: UniFile): UniFile? {
|
||||
for (chapter in chapterArchives) {
|
||||
when (Format.valueOf(chapter)) {
|
||||
is Format.Zip -> {
|
||||
// SY -->
|
||||
chapter.getZipInputStream(COMIC_INFO_FILE)?.buffered().use { stream ->
|
||||
return stream?.let { copyComicInfoFile(it, folder) }
|
||||
}
|
||||
chapter.archiveReader(context).use { reader ->
|
||||
reader.getInputStream(COMIC_INFO_FILE)?.use { stream ->
|
||||
return copyComicInfoFile(stream, folder, /* SY --> */ reader.encrypted /* SY <-- */)
|
||||
}
|
||||
is Format.Rar -> {
|
||||
val archive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
JunrarArchive(tempFileManager.createTempFile(chapter))
|
||||
} else {
|
||||
JunrarArchive(chapter.openInputStream())
|
||||
}
|
||||
|
||||
archive.use { rar ->
|
||||
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
||||
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folder: UniFile): UniFile? {
|
||||
private fun copyComicInfoFile(
|
||||
comicInfoFileStream: InputStream,
|
||||
folder: UniFile,
|
||||
// SY -->
|
||||
if (
|
||||
CbzCrypto.getPasswordProtectDlPref() &&
|
||||
CbzCrypto.isPasswordSet()
|
||||
) {
|
||||
val comicInfoArchive = folder.createFile(COMIC_INFO_ARCHIVE)
|
||||
comicInfoArchive?.addStreamToZip(comicInfoFileStream, COMIC_INFO_FILE, CbzCrypto.getDecryptedPasswordCbz())
|
||||
|
||||
return comicInfoArchive
|
||||
encrypt: Boolean,
|
||||
// SY <--
|
||||
): UniFile? {
|
||||
// SY -->
|
||||
if (encrypt) {
|
||||
val comicInfoArchiveFile = folder.createFile(COMIC_INFO_ARCHIVE)
|
||||
comicInfoArchiveFile?.let { archive ->
|
||||
ZipWriter(context, archive, encrypt = true).use { writer ->
|
||||
writer.write(comicInfoFileStream.use { it.readBytes() }, COMIC_INFO_FILE)
|
||||
}
|
||||
}
|
||||
return comicInfoArchiveFile
|
||||
} else {
|
||||
// SY <--
|
||||
return folder.createFile(COMIC_INFO_FILE)?.apply {
|
||||
@@ -344,7 +325,7 @@ actual class LocalSource(
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> = withIOContext {
|
||||
val chapters = fileSystem.getFilesInMangaDirectory(manga.url)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || Archive.isSupported(it) }
|
||||
.filter { it.isDirectory || Archive.isSupported(it) || it.extension.equals("epub", true) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
url = "${manga.url}/${chapterFile.name}"
|
||||
@@ -360,7 +341,7 @@ actual class LocalSource(
|
||||
|
||||
val format = Format.valueOf(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file, context).use { epub ->
|
||||
EpubFile(format.file.archiveReader(context)).use { epub ->
|
||||
epub.fillMetadata(manga, this)
|
||||
}
|
||||
}
|
||||
@@ -418,43 +399,25 @@ actual class LocalSource(
|
||||
|
||||
entry?.let { coverManager.update(manga, it.openInputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
// SY -->
|
||||
format.file.getCoverStreamFromZip()?.let { inputStream ->
|
||||
coverManager.update(
|
||||
manga,
|
||||
inputStream,
|
||||
format.file.isEncryptedZip()
|
||||
)
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
val rarArchive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
JunrarArchive(tempFileManager.createTempFile(format.file))
|
||||
} else {
|
||||
JunrarArchive(format.file.openInputStream())
|
||||
}
|
||||
rarArchive.use { archive ->
|
||||
// SY <--
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
is Format.Archive -> {
|
||||
format.file.archiveReader(context).use { reader ->
|
||||
val entry = reader.useEntries { entries ->
|
||||
entries
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } }
|
||||
}
|
||||
|
||||
entry?.let { coverManager.update(manga, archive.getInputStream(it)) }
|
||||
entry?.let { coverManager.update(manga, reader.getInputStream(it.name)!!, reader.encrypted) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
// SY -->
|
||||
EpubFile(format.file, context).use { epub ->
|
||||
// SY <--
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
EpubFile(format.file.archiveReader(context)).use { epub ->
|
||||
val entry = epub.getImagesFromPages().firstOrNull()
|
||||
|
||||
entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
|
||||
}
|
||||
entry?.let { coverManager.update(manga, epub.getInputStream(it)!!) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
|
||||
null
|
||||
|
||||
+4
-3
@@ -3,9 +3,8 @@ package tachiyomi.source.local.image
|
||||
import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import tachiyomi.core.common.storage.addStreamToZip
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import mihon.core.common.archive.ZipWriter
|
||||
import tachiyomi.core.common.storage.nameWithoutExtension
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||
@@ -58,7 +57,9 @@ actual class LocalCoverManager(
|
||||
inputStream.use { input ->
|
||||
// SY -->
|
||||
if (encrypted) {
|
||||
targetFile.addStreamToZip(inputStream, DEFAULT_COVER_NAME, CbzCrypto.getDecryptedPasswordCbz())
|
||||
ZipWriter(context, targetFile, encrypt = true ).use { writer ->
|
||||
writer.write(inputStream.readBytes(), DEFAULT_COVER_NAME)
|
||||
}
|
||||
DiskUtil.createNoMediaFile(directory, context)
|
||||
|
||||
manga.thumbnail_url = targetFile.uri.toString()
|
||||
|
||||
@@ -5,9 +5,9 @@ import tachiyomi.core.common.storage.extension
|
||||
|
||||
object Archive {
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "7z", "cb7", "tar", "cbt")
|
||||
|
||||
fun isSupported(file: UniFile): Boolean {
|
||||
return file.extension in SUPPORTED_ARCHIVE_TYPES
|
||||
return file.extension?.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,22 @@ package tachiyomi.source.local.io
|
||||
|
||||
import com.hippo.unifile.UniFile
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.source.local.io.Archive.isSupported as isArchiveSupported
|
||||
|
||||
sealed interface Format {
|
||||
data class Directory(val file: UniFile) : Format
|
||||
data class Zip(val file: UniFile) : Format
|
||||
data class Rar(val file: UniFile) : Format
|
||||
data class Archive(val file: UniFile) : Format
|
||||
data class Epub(val file: UniFile) : Format
|
||||
|
||||
class UnknownFormatException : Exception()
|
||||
|
||||
companion object {
|
||||
|
||||
fun valueOf(file: UniFile) = with(file) {
|
||||
when {
|
||||
isDirectory -> Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
|
||||
extension.equals("epub", true) -> Epub(this)
|
||||
else -> throw UnknownFormatException()
|
||||
}
|
||||
fun valueOf(file: UniFile) = when {
|
||||
file.isDirectory -> Directory(file)
|
||||
file.extension.equals("epub", true) -> Epub(file)
|
||||
isArchiveSupported(file) -> Archive(file)
|
||||
else -> throw UnknownFormatException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user