refactor: Use libarchive for Archive support

Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
This commit is contained in:
FooIbar 2024-06-27 10:03:41 +07:00 committed by Ahmad Ansori Palembani
parent c0bf67eabd
commit 17465f2719
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
22 changed files with 433 additions and 392 deletions

View file

@ -192,8 +192,6 @@ dependencies {
// Disk
implementation(libs.disklrucache)
implementation(libs.unifile)
implementation(libs.bundles.archive)
// HTML parser
implementation(libs.jsoup)

View file

@ -98,9 +98,6 @@
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
##---------------End: proguard configuration for kotlinx.serialization ----------
# 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.** { *; }

View file

@ -47,6 +47,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import yokai.core.archive.ZipWriter
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.getComicInfo
@ -628,25 +629,9 @@ class Downloader(
tmpDir: UniFile,
) {
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") ?: return
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")

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source
import android.content.Context
import androidx.core.net.toFile
import co.touchlab.kermit.Logger
import com.github.junrar.Archive
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
@ -17,12 +16,12 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.storage.fillChapterMetadata
import eu.kanade.tachiyomi.util.storage.fillMangaMetadata
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.extension
import eu.kanade.tachiyomi.util.system.nameWithoutExtension
import eu.kanade.tachiyomi.util.system.openReadOnlyChannel
import eu.kanade.tachiyomi.util.system.toZipFile
import eu.kanade.tachiyomi.util.system.withIOContext
import eu.kanade.tachiyomi.util.system.writeText
import kotlinx.coroutines.async
@ -35,6 +34,7 @@ import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.archive.archiveReader
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.copyFromComicInfo
@ -187,7 +187,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
val chapter = chapters.last()
val format = getFormat(chapter)
if (format is Format.Epub) {
EpubFile(format.file.openReadOnlyChannel(context)).use { epub ->
EpubFile(format.file.archiveReader(context)).use { epub ->
epub.fillMangaMetadata(manga)
}
}
@ -338,10 +338,10 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
private fun getFormat(file: UniFile) = with(file) {
val supportedArchives = listOf("zip", "cbz", "rar", "cbr", "7z", "cb7", "tar", "cbt")
when {
isDirectory -> Format.Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
supportedArchives.contains(extension?.lowercase()) -> Format.Archive(this)
extension.equals("epub", true) -> Format.Epub(this)
else -> throw Exception(context.getString(MR.strings.local_invalid_format))
}
@ -357,31 +357,22 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
entry?.let { updateCover(manga, it.openInputStream()) }
}
is Format.Zip -> {
format.file.openReadOnlyChannel(context).toZipFile().use { zip ->
val entry = zip.entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
is Format.Archive -> {
format.file.archiveReader(context).use { reader ->
val entry = reader.useEntries { entries ->
entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } }
}
entry?.let { updateCover(manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
Archive(format.file.openInputStream()).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
entry?.let { updateCover(manga, archive.getInputStream(it)) }
entry?.let { updateCover(manga, reader.getInputStream(it.name)!!) }
}
}
is Format.Epub -> {
EpubFile(format.file.openReadOnlyChannel(context)).use { epub ->
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
EpubFile(format.file.archiveReader(context)).use { epub ->
val entry = epub.getImagesFromPages().firstOrNull()
entry?.let { updateCover(manga, epub.getInputStream(it)) }
entry?.let { updateCover(manga, epub.getInputStream(it)!!) }
}
}
}
@ -401,25 +392,19 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
true
}
is Format.Epub -> {
EpubFile(format.file.openReadOnlyChannel(context)).use { epub ->
EpubFile(format.file.archiveReader(context)).use { epub ->
epub.fillChapterMetadata(chapter)
}
true
}
is Format.Rar -> Archive(format.file.openInputStream()).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && it.fileName == COMIC_INFO_FILE } ?: return false
is Format.Archive -> format.file.archiveReader(context).use { reader ->
val entry = reader.useEntries { entries ->
entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { it.isFile && it.name == COMIC_INFO_FILE }
} ?: return false
updateMetadata(chapter, manga, archive.getInputStream(entry))
true
}
is Format.Zip -> format.file.openReadOnlyChannel(context).toZipFile().use { zip ->
val entry = zip.entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && it.name == COMIC_INFO_FILE } ?: return false
updateMetadata(chapter, manga, zip.getInputStream(entry))
updateMetadata(chapter, manga, reader.getInputStream(entry.name)!!)
true
}
}
@ -442,8 +427,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
sealed class 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()
}
}

View file

@ -4,41 +4,35 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.toZipFile
import java.nio.channels.SeekableByteChannel
import yokai.core.archive.ArchiveReader
/**
* Loader used to load a chapter from a .zip or .cbz file.
*/
class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() {
/**
* The zip file to load pages from.
*/
private val zip = channel.toZipFile()
internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() {
/**
* Recycles this loader and the open zip.
*/
override fun recycle() {
super.recycle()
zip.close()
reader.close()
}
/**
* Returns the pages found on this zip archive ordered with a natural comparator.
*/
override suspend fun getPages(): List<ReaderPage> {
return zip.entries.asSequence()
.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
return 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 = { reader.getInputStream(entry.name)!! }
status = Page.State.READY
}
}
}
.toList()
}.toList()
}
/**

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context
import co.touchlab.kermit.Logger
import com.github.junrar.exception.UnsupportedRarV5Exception
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
@ -10,8 +9,8 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.system.openReadOnlyChannel
import eu.kanade.tachiyomi.util.system.withIOContext
import yokai.core.archive.archiveReader
import yokai.i18n.MR
import yokai.util.lang.getString
@ -82,13 +81,8 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) {
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
is LocalSource.Format.Zip -> ZipPageLoader(format.file.openReadOnlyChannel(context))
is LocalSource.Format.Rar -> try {
RarPageLoader(format.file.openInputStream())
} catch (e: UnsupportedRarV5Exception) {
error(context.getString(MR.strings.loader_rar5_error))
}
is LocalSource.Format.Epub -> EpubPageLoader(format.file.openReadOnlyChannel(context))
is LocalSource.Format.Archive -> ArchivePageLoader(format.file.archiveReader(context))
is LocalSource.Format.Epub -> EpubPageLoader(format.file.archiveReader(context))
}
}
else -> error(context.getString(MR.strings.source_not_installed))

View file

@ -10,10 +10,8 @@ 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 eu.kanade.tachiyomi.util.system.openReadOnlyChannel
import eu.kanade.tachiyomi.util.system.toTempFile
import uy.kohesive.injekt.injectLazy
import java.io.File
import yokai.core.archive.archiveReader
/**
* Loader used to load a chapter from the downloaded chapters.
@ -29,11 +27,11 @@ class DownloadPageLoader(
// Needed to open input streams
private val context: Application by injectLazy()
private var zipPageLoader: ZipPageLoader? = null
private var archivePageLoader: ArchivePageLoader? = null
override fun recycle() {
super.recycle()
zipPageLoader?.recycle()
archivePageLoader?.recycle()
}
/**
@ -50,7 +48,7 @@ class DownloadPageLoader(
}
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
val loader = ZipPageLoader(chapterPath.openReadOnlyChannel(context)).also { zipPageLoader = it }
val loader = ArchivePageLoader(chapterPath.archiveReader(context)).also { archivePageLoader = it }
return loader.getPages()
}
@ -66,6 +64,6 @@ class DownloadPageLoader(
}
override suspend fun loadPage(page: ReaderPage) {
zipPageLoader?.loadPage(page)
archivePageLoader?.loadPage(page)
}
}

View file

@ -3,17 +3,18 @@ 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 yokai.core.archive.ArchiveReader
import java.nio.channels.SeekableByteChannel
/**
* Loader used to load a chapter from a .epub file.
*/
class EpubPageLoader(channel: SeekableByteChannel) : PageLoader() {
class EpubPageLoader(reader: ArchiveReader) : PageLoader() {
/**
* The epub file.
*/
private val epub = EpubFile(channel)
private val epub = EpubFile(reader)
/**
* Recycles this loader and the open zip.
@ -29,7 +30,7 @@ class EpubPageLoader(channel: SeekableByteChannel) : PageLoader() {
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

View file

@ -1,79 +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 eu.kanade.tachiyomi.util.system.ImageUtil
import java.io.File
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.
*/
class RarPageLoader(inputStream: InputStream) : PageLoader() {
/**
* The rar archive to load pages from.
*/
private val archive = Archive(inputStream)
/**
* Pool for copying compressed files to an input stream.
*/
private val pool = Executors.newFixedThreadPool(1)
/**
* Recycles this loader and the open archive.
*/
override fun recycle() {
super.recycle()
archive.close()
pool.shutdown()
}
/**
* Returns an RxJava Single containing the pages found on this rar archive ordered with a natural
* comparator.
*/
override suspend fun getPages(): List<ReaderPage> {
return archive.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header ->
ReaderPage(i).apply {
stream = { getStream(header) }
status = Page.State.READY
}
}
.toList()
}
/**
* No additional action required to load the page
*/
override suspend fun loadPage(page: ReaderPage) {
check(!isRecycled)
}
/**
* 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 {
archive.extractFile(header, it)
}
} catch (e: Exception) {
}
}
return pipeIn
}
}

View file

@ -2,216 +2,58 @@ package eu.kanade.tachiyomi.util.storage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.system.toZipFile
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipFile
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
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.*
/**
* Wrapper over ZipFile to load files in epub format.
* Fills manga metadata using this epub file's metadata.
*/
class EpubFile(channel: SeekableByteChannel) : Closeable {
fun EpubFile.fillMangaMetadata(manga: SManga) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
/**
* Zip file of this epub.
*/
private val zip = channel.toZipFile()
val creator = doc.getElementsByTag("dc:creator").first()
val description = doc.getElementsByTag("dc:description").first()
/**
* Path separator used by this epub.
*/
private val pathSeparator = getPathSeparator()
manga.author = creator?.text()
manga.description = description?.text()
}
/**
* Closes the underlying zip file.
*/
override fun close() {
zip.close()
/**
* Fills chapter metadata using this epub file's metadata.
*/
fun EpubFile.fillChapterMetadata(chapter: SChapter) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val title = doc.getElementsByTag("dc:title").first()
val publisher = doc.getElementsByTag("dc:publisher").first()
val creator = doc.getElementsByTag("dc:creator").first()
var date = doc.getElementsByTag("dc:date").first()
if (date == null) {
date = doc.select("meta[property=dcterms:modified]").first()
}
/**
* Returns an input stream for reading the contents of the specified zip file entry.
*/
fun getInputStream(entry: ZipArchiveEntry): InputStream {
return zip.getInputStream(entry)
if (title != null) {
chapter.name = title.text()
}
/**
* Returns the zip file entry for the specified name, or null if not found.
*/
fun getEntry(name: String): ZipArchiveEntry? {
return zip.getEntry(name)
if (publisher != null) {
chapter.scanlator = publisher.text()
} else if (creator != null) {
chapter.scanlator = creator.text()
}
/**
* Fills manga metadata using this epub file's metadata.
*/
fun fillMangaMetadata(manga: SManga) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val creator = doc.getElementsByTag("dc:creator").first()
val description = doc.getElementsByTag("dc:description").first()
manga.author = creator?.text()
manga.description = description?.text()
}
/**
* Fills chapter metadata using this epub file's metadata.
*/
fun fillChapterMetadata(chapter: SChapter) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val title = doc.getElementsByTag("dc:title").first()
val publisher = doc.getElementsByTag("dc:publisher").first()
val creator = doc.getElementsByTag("dc:creator").first()
var date = doc.getElementsByTag("dc:date").first()
if (date == null) {
date = doc.select("meta[property=dcterms:modified]").first()
}
if (title != null) {
chapter.name = title.text()
}
if (publisher != null) {
chapter.scanlator = publisher.text()
} else if (creator != null) {
chapter.scanlator = creator.text()
}
if (date != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
try {
val parsedDate = dateFormat.parse(date.text())
if (parsedDate != null) {
chapter.date_upload = parsedDate.time
}
} catch (e: ParseException) {
// Empty
if (date != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
try {
val parsedDate = dateFormat.parse(date.text())
if (parsedDate != null) {
chapter.date_upload = parsedDate.time
}
}
}
/**
* Returns the path of all the images found in the epub file.
*/
fun getImagesFromPages(): List<String> {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val pages = getPagesFromDocument(doc)
return getImagesFromPages(pages, ref)
}
/**
* Returns the path to the package document.
*/
private fun getPackageHref(): String {
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
if (meta != null) {
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
if (path != null) {
return path
}
}
return resolveZipPath("OEBPS", "content.opf")
}
/**
* Returns the package document where all the files are listed.
*/
private fun getPackageDocument(ref: String): Document {
val entry = zip.getEntry(ref)
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
}
/**
* Returns all the pages from the epub.
*/
private fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") }
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
}
/**
* Returns all the images contained in every page from the epub.
*/
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> {
val result = mutableListOf<String>()
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 imageBasePath = getParentDirectory(entryPath)
document.allElements.forEach {
if (it.tagName() == "img") {
result.add(resolveZipPath(imageBasePath, it.attr("src")))
} else if (it.tagName() == "image") {
result.add(resolveZipPath(imageBasePath, it.attr("xlink:href")))
}
}
}
return result
}
/**
* Returns the path separator used by the epub file.
*/
private fun getPathSeparator(): String {
val meta = zip.getEntry("META-INF\\container.xml")
return if (meta != null) {
"\\"
} else {
"/"
}
}
/**
* Resolves a zip path from base and relative components and a path separator.
*/
private fun resolveZipPath(basePath: String, relativePath: String): String {
if (relativePath.startsWith(pathSeparator)) {
// Path is absolute, so return as-is.
return relativePath
}
var fixedBasePath = basePath.replace(pathSeparator, File.separator)
if (!fixedBasePath.startsWith(File.separator)) {
fixedBasePath = "${File.separator}$fixedBasePath"
}
val fixedRelativePath = relativePath.replace(pathSeparator, File.separator)
val resolvedPath = File(fixedBasePath, fixedRelativePath).canonicalPath
return resolvedPath.replace(File.separator, pathSeparator).substring(1)
}
/**
* Gets the parent directory of a path.
*/
private fun getParentDirectory(path: String): String {
val separatorIndex = path.lastIndexOf(pathSeparator)
return if (separatorIndex >= 0) {
path.substring(0, separatorIndex)
} else {
""
} catch (e: ParseException) {
// Empty
}
}
}

View file

@ -1,8 +0,0 @@
package eu.kanade.tachiyomi.util.system
import org.apache.commons.compress.archivers.zip.ZipFile
import java.nio.channels.SeekableByteChannel
fun SeekableByteChannel.toZipFile(): ZipFile {
return ZipFile.Builder().setSeekableByteChannel(this).get()
}

View file

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.os.Build
import android.os.FileUtils
import android.os.ParcelFileDescriptor
import com.hippo.unifile.UniFile
import java.io.BufferedOutputStream
import java.io.File
import java.nio.channels.SeekableByteChannel
val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.')
val UniFile.extension: String?
get() = name?.replace("${nameWithoutExtension.orEmpty()}.", "")
fun UniFile.toTempFile(context: Context): File {
val inputStream = context.contentResolver.openInputStream(uri)!!
val tempFile =
File.createTempFile(
nameWithoutExtension.orEmpty(),
null,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileUtils.copy(inputStream, tempFile.outputStream())
} else {
BufferedOutputStream(tempFile.outputStream()).use { tmpOut ->
inputStream.use { input ->
val buffer = ByteArray(8192)
var count: Int
while (input.read(buffer).also { count = it } > 0) {
tmpOut.write(buffer, 0, count)
}
}
}
}
return tempFile
}
fun UniFile.writeText(string: String, onComplete: () -> Unit = {}) {
this.openOutputStream().use {
it.write(string.toByteArray())
onComplete()
}
}
fun UniFile.openReadOnlyChannel(context: Context): SeekableByteChannel {
return ParcelFileDescriptor.AutoCloseInputStream(context.contentResolver.openFileDescriptor(uri, "r")).channel
}