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
@@ -26,6 +26,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo
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.system.ImageUtil
import tachiyomi.core.util.system.logcat
@@ -41,7 +44,6 @@ import tachiyomi.source.local.metadata.fillChapterMetadata
import tachiyomi.source.local.metadata.fillMangaMetadata
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.charset.StandardCharsets
import kotlin.time.Duration.Companion.days
@@ -96,14 +98,14 @@ actual class LocalSource(
.filter {
it.isDirectory &&
/* SY --> */ (
!it.name.startsWith('.') ||
!it.name.orEmpty().startsWith('.') ||
allowLocalSourceHiddenFolders
) /* SY <-- */
}
.distinctBy { it.name }
.filter { // Filter by query or last modified
if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true)
it.name.orEmpty().contains(query, ignoreCase = true)
} else {
it.lastModified() >= lastModifiedLimit
}
@@ -113,16 +115,16 @@ actual class LocalSource(
when (filter) {
is OrderBy.Popular -> {
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 {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
}
}
is OrderBy.Latest -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified)
mangaDirs.sortedBy(UniFile::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
mangaDirs.sortedByDescending(UniFile::lastModified)
}
}
@@ -135,13 +137,13 @@ actual class LocalSource(
// Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply {
title = mangaDir.name
url = mangaDir.name
title = mangaDir.name.orEmpty()
url = mangaDir.name.orEmpty()
// Try to find the cover
coverManager.find(mangaDir.name)
?.takeIf(File::exists)
?.let { thumbnail_url = it.absolutePath }
coverManager.find(mangaDir.name.orEmpty())
?.takeIf(UniFile::exists)
?.let { thumbnail_url = it.uri.toString() }
}
}
@@ -170,7 +172,7 @@ actual class LocalSource(
// SY -->
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()
} ?: return
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
@@ -188,7 +190,7 @@ actual class LocalSource(
// Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.absolutePath
manga.thumbnail_url = it.uri.toString()
}
// Augment manga details based on metadata files
@@ -211,11 +213,11 @@ actual class LocalSource(
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga)
}
// SY -->
comicInfoArchiveFile != null -> {
val comicInfoArchive = ZipFile(comicInfoArchiveFile)
val comicInfoArchive = ZipFile(comicInfoArchiveFile.toFile())
noXmlFile?.delete()
if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) {
@@ -229,7 +231,7 @@ actual class LocalSource(
// Old custom JSON format
// TODO: remove support for this entirely after a while
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.openInputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
@@ -239,7 +241,7 @@ actual class LocalSource(
}
// Replace with ComicInfo.xml file
val comicInfo = manga.getComicInfo()
UniFile.fromFile(mangaDir)
mangaDir
?.createFile(COMIC_INFO_FILE)
?.openOutputStream()
?.use {
@@ -255,7 +257,7 @@ actual class LocalSource(
.filter(Archive::isSupported)
.toList()
val folderPath = mangaDir?.absolutePath
val folderPath = mangaDir?.filePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
// SY -->
@@ -281,11 +283,11 @@ actual class LocalSource(
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) {
when (Format.valueOf(chapter)) {
is Format.Zip -> {
ZipFile(chapter).use { zip: ZipFile ->
ZipFile(chapter.toFile()).use { zip: ZipFile ->
// SY -->
if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())
) {
@@ -304,7 +306,7 @@ actual class LocalSource(
}
}
is Format.Rar -> {
JunrarArchive(chapter).use { rar ->
JunrarArchive(chapter.toFile()).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
@@ -359,9 +361,9 @@ actual class LocalSource(
SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) {
chapterFile.name
chapterFile.name.orEmpty()
} else {
chapterFile.nameWithoutExtension
chapterFile.nameWithoutExtension.orEmpty()
}
date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition
@@ -391,8 +393,8 @@ actual class LocalSource(
fun getFormat(chapter: SChapter): Format {
try {
return File(fileSystem.getBaseDirectory(), chapter.url)
.takeIf { it.exists() }
return fileSystem.getBaseDirectory()
?.findFile(chapter.url)
?.let(Format.Companion::valueOf)
?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
} 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 {
when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
?.sortedWith { f1, f2 ->
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 -> {
ZipFile(format.file).use { zip ->
ZipFile(format.file.toFile()).use { zip ->
// SY -->
var encrypted = false
if (zip.isEncrypted) {
@@ -428,7 +436,7 @@ actual class LocalSource(
}
}
is Format.Rar -> {
JunrarArchive(format.file).use { archive ->
JunrarArchive(format.file.toFile()).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.DiskUtil
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.ZipParameters
import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem
import java.io.File
@@ -20,13 +22,13 @@ actual class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem,
) {
actual fun find(mangaUrl: String): File? {
actual fun find(mangaUrl: String): UniFile? {
return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with "cover"
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image
.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 -->
encrypted: Boolean,
// SY <--
): File? {
): UniFile? {
val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) {
inputStream.close()
@@ -46,38 +48,38 @@ actual class LocalCoverManager(
var targetFile = find(manga.url)
if (targetFile == null) {
// SY -->
if (encrypted) {
targetFile = File(directory.absolutePath, COVER_ARCHIVE_NAME)
targetFile = if (encrypted) {
directory.createFile(COVER_ARCHIVE_NAME)
} else {
targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME)
targetFile.createNewFile()
directory.createFile(DEFAULT_COVER_NAME)
}
// SY <--
}
targetFile!!
// It might not exist at this point
targetFile.parentFile?.mkdirs()
inputStream.use { input ->
// SY -->
if (encrypted) {
val zip4j = ZipFile(targetFile)
val zip4j = ZipFile(targetFile.toFile())
val zipParameters = ZipParameters()
zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
CbzCrypto.setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = DEFAULT_COVER_NAME
zip4j.addStream(input, zipParameters)
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
DiskUtil.createNoMediaFile(directory, context)
manga.thumbnail_url = zip4j.file.absolutePath
return zip4j.file
manga.thumbnail_url = targetFile.uri.toString()
return targetFile
} else {
// SY <--
targetFile.outputStream().use { output ->
targetFile.openOutputStream().use { output ->
input.copyTo(output)
}
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
manga.thumbnail_url = targetFile.absolutePath
DiskUtil.createNoMediaFile(directory, context)
manga.thumbnail_url = targetFile.uri.toString()
return targetFile
}
}
@@ -1,27 +1,31 @@
package tachiyomi.source.local.io
import tachiyomi.core.provider.FolderProvider
import java.io.File
import android.content.Context
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import tachiyomi.core.storage.FolderProvider
actual class LocalSourceFileSystem(
private val context: Context,
private val folderProvider: FolderProvider,
) {
actual fun getBaseDirectory(): File {
return File(folderProvider.directory(), "local")
actual fun getBaseDirectory(): UniFile? {
return UniFile.fromUri(context, folderProvider.path().toUri())
?.createDirectory("local")
}
actual fun getFilesInBaseDirectory(): List<File> {
return getBaseDirectory().listFiles().orEmpty().toList()
actual fun getFilesInBaseDirectory(): List<UniFile> {
return getBaseDirectory()?.listFiles().orEmpty().toList()
}
actual fun getMangaDirectory(name: String): File? {
actual fun getMangaDirectory(name: String): UniFile? {
return getFilesInBaseDirectory()
// Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == name }
}
actual fun getFilesInMangaDirectory(name: String): List<File> {
actual fun getFilesInMangaDirectory(name: String): List<UniFile> {
return getFilesInBaseDirectory()
// Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == name }
@@ -1,14 +1,14 @@
package tachiyomi.source.local.image
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga
import java.io.File
import java.io.InputStream
expect class LocalCoverManager {
fun find(mangaUrl: String): File?
fun find(mangaUrl: String): UniFile?
// SY -->
fun update(manga: SManga, inputStream: InputStream, encrypted: Boolean = false): File?
fun update(manga: SManga, inputStream: InputStream, encrypted: Boolean = false): UniFile?
// SY <--
}
@@ -1,12 +1,13 @@
package tachiyomi.source.local.io
import java.io.File
import com.hippo.unifile.UniFile
import tachiyomi.core.storage.extension
object Archive {
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
fun isSupported(file: File): Boolean = with(file) {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
fun isSupported(file: UniFile): Boolean {
return file.extension in SUPPORTED_ARCHIVE_TYPES
}
}
@@ -1,18 +1,19 @@
package tachiyomi.source.local.io
import java.io.File
import com.hippo.unifile.UniFile
import tachiyomi.core.storage.extension
sealed interface Format {
data class Directory(val file: File) : Format
data class Zip(val file: File) : Format
data class Rar(val file: File) : Format
data class Epub(val file: File) : Format
data class Directory(val file: UniFile) : Format
data class Zip(val file: UniFile) : Format
data class Rar(val file: UniFile) : Format
data class Epub(val file: UniFile) : Format
class UnknownFormatException : Exception()
companion object {
fun valueOf(file: File) = with(file) {
fun valueOf(file: UniFile) = with(file) {
when {
isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
@@ -1,14 +1,14 @@
package tachiyomi.source.local.io
import java.io.File
import com.hippo.unifile.UniFile
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>
}