Use UniFile for local source file handling

(cherry picked from commit ca5498434409d4085c404f4ff5ed5e608f430a3b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt
#	core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
#	source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
#	source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
#	source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
This commit is contained in:
arkon 2023-11-26 15:59:31 -05:00 committed by Jobobby04
parent bda2ef3eee
commit 927c94041e
20 changed files with 125 additions and 101 deletions

View File

@ -30,7 +30,7 @@ import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.provider.AndroidStorageFolderProvider import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.AndroidDatabaseHandler
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
@ -153,7 +153,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ImageSaver(app) } addSingletonFactory { ImageSaver(app) }
addSingletonFactory { AndroidStorageFolderProvider(app) } addSingletonFactory { AndroidStorageFolderProvider(app) }
addSingletonFactory { LocalSourceFileSystem(get<AndroidStorageFolderProvider>()) } addSingletonFactory { LocalSourceFileSystem(app, get<AndroidStorageFolderProvider>()) }
addSingletonFactory { LocalCoverManager(app, get()) } addSingletonFactory { LocalCoverManager(app, get()) }
// SY --> // SY -->

View File

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.AndroidPreferenceStore
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.AndroidStorageFolderProvider import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences

View File

@ -1,25 +1,24 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.io.FileInputStream
/** /**
* Loader used to load a chapter from a directory given on [file]. * Loader used to load a chapter from a directory given on [file].
*/ */
internal class DirectoryPageLoader(val file: File) : PageLoader() { internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
override var isLocal: Boolean = true override var isLocal: Boolean = true
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
return file.listFiles() return file.listFiles()
?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } }
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
?.mapIndexed { i, file -> ?.mapIndexed { i, file ->
val streamFn = { FileInputStream(file) } val streamFn = { file.openInputStream() }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.State.READY status = Page.State.READY

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
/** /**
* Loader used to load a chapter from the downloaded chapters. * Loader used to load a chapter from the downloaded chapters.
@ -47,7 +46,7 @@ internal class DownloadPageLoader(
} }
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> { private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it } val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it }
return loader.getPages() return loader.getPages()
} }

View File

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File
/** /**
* Loader used to load a chapter from a .epub file. * Loader used to load a chapter from a .epub file.
*/ */
internal class EpubPageLoader(file: File) : PageLoader() { internal class EpubPageLoader(file: UniFile) : PageLoader() {
private val epub = EpubFile(file) private val epub = EpubFile(file)

View File

@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application import android.app.Application
import com.github.junrar.Archive import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader import com.github.junrar.rarfile.FileHeader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -17,9 +19,9 @@ import java.io.PipedOutputStream
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
*/ */
internal class RarPageLoader(file: File) : PageLoader() { internal class RarPageLoader(file: UniFile) : PageLoader() {
private val rar = Archive(file) private val rar = Archive(file.toFile())
// SY --> // SY -->
private val context: Application by injectLazy() private val context: Application by injectLazy()
@ -31,7 +33,7 @@ internal class RarPageLoader(file: File) : PageLoader() {
init { init {
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
tmpDir.mkdirs() tmpDir.mkdirs()
Archive(file).use { rar -> Archive(file.toFile()).use { rar ->
rar.fileHeaders.asSequence() rar.fileHeaders.asSequence()
.filterNot { it.isDirectory } .filterNot { it.isDirectory }
.forEach { header -> .forEach { header ->
@ -52,7 +54,7 @@ internal class RarPageLoader(file: File) : PageLoader() {
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
// SY --> // SY -->
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
return DirectoryPageLoader(tmpDir).getPages() return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages()
} }
// SY <-- // SY <--
return rar.fileHeaders.asSequence() return rar.fileHeaders.asSequence()

View File

@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application import android.app.Application
import android.os.Build import android.os.Build
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -19,7 +21,7 @@ import net.lingala.zip4j.ZipFile as Zip4jFile
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
internal class ZipPageLoader(file: File) : PageLoader() { internal class ZipPageLoader(file: UniFile) : PageLoader() {
// SY --> // SY -->
private val context: Application by injectLazy() private val context: Application by injectLazy()
@ -27,12 +29,12 @@ internal class ZipPageLoader(file: File) : PageLoader() {
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
it.deleteRecursively() it.deleteRecursively()
} }
private val zip4j: Zip4jFile = Zip4jFile(file) private val zip4j: Zip4jFile = Zip4jFile(file.toFile())
private val zip: ZipFile? = private val zip: ZipFile? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!zip4j.isEncrypted) ZipFile(file, StandardCharsets.ISO_8859_1) else null if (!zip4j.isEncrypted) ZipFile(file.toFile(), StandardCharsets.ISO_8859_1) else null
} else { } else {
if (!zip4j.isEncrypted) ZipFile(file) else null if (!zip4j.isEncrypted) ZipFile(file.toFile()) else null
} }
init { init {
@ -40,7 +42,7 @@ internal class ZipPageLoader(file: File) : PageLoader() {
zip4j.charset = StandardCharsets.ISO_8859_1 zip4j.charset = StandardCharsets.ISO_8859_1
} }
Zip4jFile(file).use { zip -> Zip4jFile(file.toFile()).use { zip ->
if (zip.isEncrypted) { if (zip.isEncrypted) {
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
this.recycle() this.recycle()
@ -79,7 +81,7 @@ internal class ZipPageLoader(file: File) : PageLoader() {
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
return DirectoryPageLoader(tmpDir).getPages() return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages()
} }
if (zip == null) { if (zip == null) {

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.util.storage package eu.kanade.tachiyomi.util.storage
import com.hippo.unifile.UniFile
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import tachiyomi.core.storage.toFile
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -11,12 +13,12 @@ import java.util.zip.ZipFile
/** /**
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: File) : Closeable { class EpubFile(file: UniFile) : Closeable {
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = ZipFile(file) private val zip = ZipFile(file.toFile())
/** /**
* Path separator used by this epub. * Path separator used by this epub.

View File

@ -1,4 +1,4 @@
package tachiyomi.core.provider package tachiyomi.core.storage
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment

View File

@ -1,4 +1,4 @@
package tachiyomi.core.provider package tachiyomi.core.storage
import java.io.File import java.io.File

View File

@ -1,9 +1,12 @@
package tachiyomi.core.storage package tachiyomi.core.storage
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import java.io.File
val UniFile.extension: String? val UniFile.extension: String?
get() = name?.substringAfterLast('.') get() = name?.substringAfterLast('.')
val UniFile.nameWithoutExtension: String? val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.') get() = name?.substringBeforeLast('.')
fun UniFile.toFile(): File? = filePath?.let { File(it) }

View File

@ -43,7 +43,8 @@ import kotlin.math.min
object ImageUtil { object ImageUtil {
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean { fun isImage(name: String?, openStream: (() -> InputStream)? = null): Boolean {
if (name == null) return false
// SY --> // SY -->
if (File(name).extension.equals("cbi", ignoreCase = true)) return true if (File(name).extension.equals("cbi", ignoreCase = true)) return true
// SY <-- // SY <--

View File

@ -1,7 +1,7 @@
package tachiyomi.domain.storage.service package tachiyomi.domain.storage.service
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.FolderProvider import tachiyomi.core.storage.FolderProvider
class StoragePreferences( class StoragePreferences(
private val folderProvider: FolderProvider, private val folderProvider: FolderProvider,

View File

@ -26,6 +26,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.storage.extension
import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -41,7 +44,6 @@ import tachiyomi.source.local.metadata.fillChapterMetadata
import tachiyomi.source.local.metadata.fillMangaMetadata import tachiyomi.source.local.metadata.fillMangaMetadata
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
@ -96,14 +98,14 @@ actual class LocalSource(
.filter { .filter {
it.isDirectory && it.isDirectory &&
/* SY --> */ ( /* SY --> */ (
!it.name.startsWith('.') || !it.name.orEmpty().startsWith('.') ||
allowLocalSourceHiddenFolders allowLocalSourceHiddenFolders
) /* SY <-- */ ) /* SY <-- */
} }
.distinctBy { it.name } .distinctBy { it.name }
.filter { // Filter by query or last modified .filter { // Filter by query or last modified
if (lastModifiedLimit == 0L) { if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true) it.name.orEmpty().contains(query, ignoreCase = true)
} else { } else {
it.lastModified() >= lastModifiedLimit it.lastModified() >= lastModifiedLimit
} }
@ -113,16 +115,16 @@ actual class LocalSource(
when (filter) { when (filter) {
is OrderBy.Popular -> { is OrderBy.Popular -> {
mangaDirs = if (filter.state!!.ascending) { mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
} else { } else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }) mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
} }
} }
is OrderBy.Latest -> { is OrderBy.Latest -> {
mangaDirs = if (filter.state!!.ascending) { mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified) mangaDirs.sortedBy(UniFile::lastModified)
} else { } else {
mangaDirs.sortedByDescending(File::lastModified) mangaDirs.sortedByDescending(UniFile::lastModified)
} }
} }
@ -135,13 +137,13 @@ actual class LocalSource(
// Transform mangaDirs to list of SManga // Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir -> val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply { SManga.create().apply {
title = mangaDir.name title = mangaDir.name.orEmpty()
url = mangaDir.name url = mangaDir.name.orEmpty()
// Try to find the cover // Try to find the cover
coverManager.find(mangaDir.name) coverManager.find(mangaDir.name.orEmpty())
?.takeIf(File::exists) ?.takeIf(UniFile::exists)
?.let { thumbnail_url = it.absolutePath } ?.let { thumbnail_url = it.uri.toString() }
} }
} }
@ -170,7 +172,7 @@ actual class LocalSource(
// SY --> // SY -->
fun updateMangaInfo(manga: SManga) { fun updateMangaInfo(manga: SManga) {
val directory = fileSystem.getFilesInBaseDirectories().map { File(it, manga.url) }.find { val directory = fileSystem.getFilesInBaseDirectory().map { File(it.toFile(), manga.url) }.find {
it.exists() it.exists()
} ?: return } ?: return
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
@ -188,7 +190,7 @@ actual class LocalSource(
// Manga details related // Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
coverManager.find(manga.url)?.let { coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.absolutePath manga.thumbnail_url = it.uri.toString()
} }
// Augment manga details based on metadata files // Augment manga details based on metadata files
@ -211,11 +213,11 @@ actual class LocalSource(
// Top level ComicInfo.xml // Top level ComicInfo.xml
comicInfoFile != null -> { comicInfoFile != null -> {
noXmlFile?.delete() noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga) setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga)
} }
// SY --> // SY -->
comicInfoArchiveFile != null -> { comicInfoArchiveFile != null -> {
val comicInfoArchive = ZipFile(comicInfoArchiveFile) val comicInfoArchive = ZipFile(comicInfoArchiveFile.toFile())
noXmlFile?.delete() noXmlFile?.delete()
if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) { if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) {
@ -229,7 +231,7 @@ actual class LocalSource(
// Old custom JSON format // Old custom JSON format
// TODO: remove support for this entirely after a while // TODO: remove support for this entirely after a while
legacyJsonDetailsFile != null -> { legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run { json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.openInputStream()).run {
title?.let { manga.title = it } title?.let { manga.title = it }
author?.let { manga.author = it } author?.let { manga.author = it }
artist?.let { manga.artist = it } artist?.let { manga.artist = it }
@ -239,7 +241,7 @@ actual class LocalSource(
} }
// Replace with ComicInfo.xml file // Replace with ComicInfo.xml file
val comicInfo = manga.getComicInfo() val comicInfo = manga.getComicInfo()
UniFile.fromFile(mangaDir) mangaDir
?.createFile(COMIC_INFO_FILE) ?.createFile(COMIC_INFO_FILE)
?.openOutputStream() ?.openOutputStream()
?.use { ?.use {
@ -255,7 +257,7 @@ actual class LocalSource(
.filter(Archive::isSupported) .filter(Archive::isSupported)
.toList() .toList()
val folderPath = mangaDir?.absolutePath val folderPath = mangaDir?.filePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
// SY --> // SY -->
@ -281,11 +283,11 @@ actual class LocalSource(
return@withIOContext manga return@withIOContext manga
} }
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? { private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? {
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
ZipFile(chapter).use { zip: ZipFile -> ZipFile(chapter.toFile()).use { zip: ZipFile ->
// SY --> // SY -->
if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz()) if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())
) { ) {
@ -304,7 +306,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(chapter).use { rar -> JunrarArchive(chapter.toFile()).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream -> rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@ -359,9 +361,9 @@ actual class LocalSource(
SChapter.create().apply { SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}" url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) { name = if (chapterFile.isDirectory) {
chapterFile.name chapterFile.name.orEmpty()
} else { } else {
chapterFile.nameWithoutExtension chapterFile.nameWithoutExtension.orEmpty()
} }
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition chapter_number = ChapterRecognition
@ -391,8 +393,8 @@ actual class LocalSource(
fun getFormat(chapter: SChapter): Format { fun getFormat(chapter: SChapter): Format {
try { try {
return File(fileSystem.getBaseDirectory(), chapter.url) return fileSystem.getBaseDirectory()
.takeIf { it.exists() } ?.findFile(chapter.url)
?.let(Format.Companion::valueOf) ?.let(Format.Companion::valueOf)
?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
} catch (e: Format.UnknownFormatException) { } catch (e: Format.UnknownFormatException) {
@ -402,18 +404,24 @@ actual class LocalSource(
} }
} }
private fun updateCover(chapter: SChapter, manga: SManga): File? { private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
return try { return try {
when (val format = getFormat(chapter)) { when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry = format.file.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } ?.sortedWith { f1, f2 ->
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(
f2.name.orEmpty(),
)
}
?.find {
!it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() }
}
entry?.let { coverManager.update(manga, it.inputStream()) } entry?.let { coverManager.update(manga, it.openInputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile(format.file.toFile()).use { zip ->
// SY --> // SY -->
var encrypted = false var encrypted = false
if (zip.isEncrypted) { if (zip.isEncrypted) {
@ -428,7 +436,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(format.file).use { archive -> JunrarArchive(format.file.toFile()).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }

View File

@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.ZipParameters
import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.io.LocalSourceFileSystem
import java.io.File import java.io.File
@ -20,13 +22,13 @@ actual class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem, private val fileSystem: LocalSourceFileSystem,
) { ) {
actual fun find(mangaUrl: String): File? { actual fun find(mangaUrl: String): UniFile? {
return fileSystem.getFilesInMangaDirectory(mangaUrl) return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with "cover" // Get all file whose names start with "cover"
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image // Get the first actual image
.firstOrNull { .firstOrNull {
ImageUtil.isImage(it.name) { it.inputStream() } || it.name == COVER_ARCHIVE_NAME ImageUtil.isImage(it.name) { it.openInputStream() } || it.name == COVER_ARCHIVE_NAME
} }
} }
@ -36,7 +38,7 @@ actual class LocalCoverManager(
// SY --> // SY -->
encrypted: Boolean, encrypted: Boolean,
// SY <-- // SY <--
): File? { ): UniFile? {
val directory = fileSystem.getMangaDirectory(manga.url) val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) { if (directory == null) {
inputStream.close() inputStream.close()
@ -46,38 +48,38 @@ actual class LocalCoverManager(
var targetFile = find(manga.url) var targetFile = find(manga.url)
if (targetFile == null) { if (targetFile == null) {
// SY --> // SY -->
if (encrypted) { targetFile = if (encrypted) {
targetFile = File(directory.absolutePath, COVER_ARCHIVE_NAME) directory.createFile(COVER_ARCHIVE_NAME)
} else { } else {
targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME) directory.createFile(DEFAULT_COVER_NAME)
targetFile.createNewFile()
} }
// SY <-- // SY <--
} }
targetFile!!
// It might not exist at this point // It might not exist at this point
targetFile.parentFile?.mkdirs()
inputStream.use { input -> inputStream.use { input ->
// SY --> // SY -->
if (encrypted) { if (encrypted) {
val zip4j = ZipFile(targetFile) val zip4j = ZipFile(targetFile.toFile())
val zipParameters = ZipParameters() val zipParameters = ZipParameters()
zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz()) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
CbzCrypto.setZipParametersEncrypted(zipParameters) CbzCrypto.setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = DEFAULT_COVER_NAME zipParameters.fileNameInZip = DEFAULT_COVER_NAME
zip4j.addStream(input, zipParameters) zip4j.addStream(input, zipParameters)
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context) DiskUtil.createNoMediaFile(directory, context)
manga.thumbnail_url = zip4j.file.absolutePath manga.thumbnail_url = targetFile.uri.toString()
return zip4j.file return targetFile
} else { } else {
// SY <-- // SY <--
targetFile.outputStream().use { output -> targetFile.openOutputStream().use { output ->
input.copyTo(output) input.copyTo(output)
} }
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context) DiskUtil.createNoMediaFile(directory, context)
manga.thumbnail_url = targetFile.absolutePath manga.thumbnail_url = targetFile.uri.toString()
return targetFile return targetFile
} }
} }

View File

@ -1,27 +1,31 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import tachiyomi.core.provider.FolderProvider import android.content.Context
import java.io.File import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import tachiyomi.core.storage.FolderProvider
actual class LocalSourceFileSystem( actual class LocalSourceFileSystem(
private val context: Context,
private val folderProvider: FolderProvider, private val folderProvider: FolderProvider,
) { ) {
actual fun getBaseDirectory(): File { actual fun getBaseDirectory(): UniFile? {
return File(folderProvider.directory(), "local") return UniFile.fromUri(context, folderProvider.path().toUri())
?.createDirectory("local")
} }
actual fun getFilesInBaseDirectory(): List<File> { actual fun getFilesInBaseDirectory(): List<UniFile> {
return getBaseDirectory().listFiles().orEmpty().toList() return getBaseDirectory()?.listFiles().orEmpty().toList()
} }
actual fun getMangaDirectory(name: String): File? { actual fun getMangaDirectory(name: String): UniFile? {
return getFilesInBaseDirectory() return getFilesInBaseDirectory()
// Get the first mangaDir or null // Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == name } .firstOrNull { it.isDirectory && it.name == name }
} }
actual fun getFilesInMangaDirectory(name: String): List<File> { actual fun getFilesInMangaDirectory(name: String): List<UniFile> {
return getFilesInBaseDirectory() return getFilesInBaseDirectory()
// Filter out ones that are not related to the manga and is not a directory // Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == name } .filter { it.isDirectory && it.name == name }

View File

@ -1,14 +1,14 @@
package tachiyomi.source.local.image package tachiyomi.source.local.image
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import java.io.File
import java.io.InputStream import java.io.InputStream
expect class LocalCoverManager { expect class LocalCoverManager {
fun find(mangaUrl: String): File? fun find(mangaUrl: String): UniFile?
// SY --> // SY -->
fun update(manga: SManga, inputStream: InputStream, encrypted: Boolean = false): File? fun update(manga: SManga, inputStream: InputStream, encrypted: Boolean = false): UniFile?
// SY <-- // SY <--
} }

View File

@ -1,12 +1,13 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import java.io.File import com.hippo.unifile.UniFile
import tachiyomi.core.storage.extension
object Archive { object Archive {
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
fun isSupported(file: File): Boolean = with(file) { fun isSupported(file: UniFile): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES return file.extension in SUPPORTED_ARCHIVE_TYPES
} }
} }

View File

@ -1,18 +1,19 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import java.io.File import com.hippo.unifile.UniFile
import tachiyomi.core.storage.extension
sealed interface Format { sealed interface Format {
data class Directory(val file: File) : Format data class Directory(val file: UniFile) : Format
data class Zip(val file: File) : Format data class Zip(val file: UniFile) : Format
data class Rar(val file: File) : Format data class Rar(val file: UniFile) : Format
data class Epub(val file: File) : Format data class Epub(val file: UniFile) : Format
class UnknownFormatException : Exception() class UnknownFormatException : Exception()
companion object { companion object {
fun valueOf(file: File) = with(file) { fun valueOf(file: UniFile) = with(file) {
when { when {
isDirectory -> Directory(this) isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)

View File

@ -1,14 +1,14 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import java.io.File import com.hippo.unifile.UniFile
expect class LocalSourceFileSystem { expect class LocalSourceFileSystem {
fun getBaseDirectory(): File fun getBaseDirectory(): UniFile?
fun getFilesInBaseDirectory(): List<File> fun getFilesInBaseDirectory(): List<UniFile>
fun getMangaDirectory(name: String): File? fun getMangaDirectory(name: String): UniFile?
fun getFilesInMangaDirectory(name: String): List<File> fun getFilesInMangaDirectory(name: String): List<UniFile>
} }