Remove uses of File#listFiles()

pull/449/head
Isira Seneviratne 3 years ago
parent df05211561
commit 9f7ec888a6

@ -1,7 +1,8 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.local.data.ImageFileFilter
import org.koitharu.kotatsu.local.data.hasImageExtension
import org.koitharu.kotatsu.local.data.isImageExtension
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import java.util.Date
@ -17,11 +18,8 @@ data class Bookmark(
val percent: Float,
) : ListModel {
val directImageUrl: String?
get() = if (isImageUrlDirect()) imageUrl else null
val imageLoadData: Any
get() = if (isImageUrlDirect()) imageUrl else toMangaPage()
get() = if (hasImageExtension(imageUrl)) imageUrl else toMangaPage()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Bookmark &&
@ -36,9 +34,4 @@ data class Bookmark(
preview = null,
source = manga.source,
)
private fun isImageUrlDirect(): Boolean {
val extension = imageUrl.substringAfterLast('.')
return extension.isNotEmpty() && ImageFileFilter().isExtensionValid(extension)
}
}

@ -7,14 +7,11 @@ import android.os.Build
import android.os.Environment
import android.os.storage.StorageManager
import android.provider.OpenableColumns
import androidx.annotation.WorkerThread
import androidx.core.database.getStringOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import java.io.File
import java.io.FileFilter
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@ -46,8 +43,6 @@ fun File.getStorageName(context: Context): String = runCatching {
}
}.getOrNull() ?: context.getString(R.string.other_storage)
fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null
suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
delete() || deleteRecursively()
}
@ -66,33 +61,3 @@ fun ContentResolver.resolveName(uri: Uri): String? {
}
return fallback
}
suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
computeSizeInternal(this)
}
@WorkerThread
private fun computeSizeInternal(file: File): Long {
if (file.isDirectory) {
val files = file.listFiles() ?: return 0L
return files.sumOf { computeSizeInternal(it) }
} else {
return file.length()
}
}
fun File.listFilesRecursive(filter: FileFilter? = null): Sequence<File> = sequence {
listFilesRecursiveImpl(this@listFilesRecursive, filter)
}
private suspend fun SequenceScope<File>.listFilesRecursiveImpl(root: File, filter: FileFilter?) {
val ss = root.list() ?: return
for (s in ss) {
val f = File(root, s)
if (f.isDirectory) {
listFilesRecursiveImpl(f, filter)
} else if (filter == null || filter.accept(f)) {
yield(f)
}
}
}

@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.util.ext
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.Path
import kotlin.io.path.fileSize
import kotlin.io.path.walk
@OptIn(ExperimentalPathApi::class)
suspend fun Path.computeSize(): Long = runInterruptible(Dispatchers.IO) {
// Directories are not included by default
walk().sumOf { it.fileSize() }
}
fun Uri.toPathOrNull() = if (scheme == "file") path?.let { Path(it) } else null

@ -4,11 +4,15 @@ import androidx.annotation.WorkerThread
import androidx.collection.ArraySet
import okio.Closeable
import java.io.File
import java.io.FileInputStream
import java.nio.file.Path
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import kotlin.io.path.forEachDirectoryEntry
import kotlin.io.path.inputStream
import kotlin.io.path.isDirectory
import kotlin.io.path.name
class ZipOutput(
val file: File,
@ -23,7 +27,7 @@ class ZipOutput(
@WorkerThread
fun put(name: String, file: File): Boolean {
return output.appendFile(file, name)
return output.appendFile(file.toPath(), name)
}
@WorkerThread
@ -78,8 +82,8 @@ class ZipOutput(
}
@WorkerThread
private fun ZipOutputStream.appendFile(fileToZip: File, name: String): Boolean {
if (fileToZip.isDirectory) {
private fun ZipOutputStream.appendFile(fileToZip: Path, name: String): Boolean {
if (fileToZip.isDirectory()) {
val entry = if (name.endsWith("/")) {
ZipEntry(name)
} else {
@ -90,11 +94,11 @@ class ZipOutput(
}
putNextEntry(entry)
closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
appendFile(childFile, "$name/${childFile.name}")
fileToZip.forEachDirectoryEntry {
appendFile(it, "$name/${it.name}")
}
} else {
FileInputStream(fileToZip).use { fis ->
fileToZip.inputStream().use { fis ->
if (!entryNames.add(name)) {
return false
}

@ -44,7 +44,7 @@ import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.toPathOrNull
import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
@ -135,13 +135,7 @@ class DetailsViewModel @Inject constructor(
val localSize = doubleManga
.map {
val local = it?.local
if (local != null) {
val file = local.url.toUri().toFileOrNull()
file?.computeSize() ?: 0L
} else {
0L
}
it?.local?.url?.toUri()?.toPathOrNull()?.computeSize() ?: 0L
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
val description = manga

@ -1,31 +1,13 @@
package org.koitharu.kotatsu.local.data
import android.net.Uri
import java.io.File
import java.io.FileFilter
import java.io.FilenameFilter
import java.util.Locale
class CbzFilter : FileFilter, FilenameFilter {
override fun accept(dir: File, name: String): Boolean {
return isFileSupported(name)
}
override fun accept(pathname: File?): Boolean {
return isFileSupported(pathname?.name ?: return false)
}
companion object {
fun isFileSupported(name: String): Boolean {
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
return ext == "cbz" || ext == "zip"
}
fun isCbzExtension(string: String): Boolean {
return string.equals("cbz", ignoreCase = true) || string.equals("zip", ignoreCase = true)
}
fun isUriSupported(uri: Uri): Boolean {
val scheme = uri.scheme?.lowercase(Locale.ROOT)
return scheme != null && scheme == "cbz" || scheme == "zip"
}
}
fun hasCbzExtension(name: String): Boolean {
return isCbzExtension(name.substringAfterLast('.', ""))
}
fun hasCbzExtension(uri: Uri) = uri.scheme?.let { isCbzExtension(it) } ?: false

@ -1,29 +1,16 @@
package org.koitharu.kotatsu.local.data
import java.io.File
import java.io.FileFilter
import java.io.FilenameFilter
import java.util.Locale
import java.util.zip.ZipEntry
class ImageFileFilter : FilenameFilter, FileFilter {
override fun accept(dir: File, name: String): Boolean {
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
return isExtensionValid(ext)
}
override fun accept(pathname: File?): Boolean {
val ext = pathname?.extension?.lowercase(Locale.ROOT) ?: return false
return isExtensionValid(ext)
}
fun isImageExtension(string: String): Boolean {
return string.equals("png", ignoreCase = true) || string.equals("jpg", ignoreCase = true)
|| string.equals("jpeg", ignoreCase = true) || string.equals("webp", ignoreCase = true)
}
fun accept(entry: ZipEntry): Boolean {
val ext = entry.name.substringAfterLast('.', "").lowercase(Locale.ROOT)
return isExtensionValid(ext)
}
fun hasImageExtension(name: String): Boolean {
return isImageExtension(name.substringAfterLast('.', ""))
}
fun isExtensionValid(ext: String): Boolean {
return ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "webp"
}
fun hasImageExtension(entry: ZipEntry): Boolean {
return hasImageExtension(entry.name)
}

@ -14,6 +14,7 @@ import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.CompositeMutex
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@ -32,6 +33,9 @@ import java.io.File
import java.util.EnumSet
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.io.path.deleteExisting
import kotlin.io.path.forEachDirectoryEntry
import kotlin.io.path.useDirectoryEntries
private const val MAX_PARALLELISM = 4
@ -57,10 +61,7 @@ class LocalMangaRepository @Inject constructor(
if (offset > 0) {
return emptyList()
}
val list = getRawList()
if (query.isNotEmpty()) {
list.retainAll { x -> x.isMatchesQuery(query) }
}
val list = getRawList().filter { query.isEmpty() || it.isMatchesQuery(query) }
return list.unwrap()
}
@ -69,18 +70,17 @@ class LocalMangaRepository @Inject constructor(
return emptyList()
}
val list = getRawList()
if (!tags.isNullOrEmpty()) {
list.retainAll { x -> x.containsTags(tags) }
}
when (sortOrder) {
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.manga.title })
SortOrder.RATING -> list.sortByDescending { it.manga.rating }
SortOrder.NEWEST,
SortOrder.UPDATED,
-> list.sortByDescending { it.createdAt }
else -> Unit
}
.filter { tags.isNullOrEmpty() || it.containsTags(tags) }
.apply {
when (sortOrder) {
SortOrder.ALPHABETICAL -> sortedWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
SortOrder.RATING -> sortedByDescending { it.manga.rating }
SortOrder.NEWEST, SortOrder.UPDATED -> sortedByDescending { it.createdAt }
else -> {
}
}
}
return list.unwrap()
}
@ -127,21 +127,19 @@ class LocalMangaRepository @Inject constructor(
}
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
val files = getAllFiles()
if (files.isEmpty()) {
return null
}
return channelFlow {
for (file in files) {
launch {
val mangaInput = LocalMangaInput.of(file)
runCatchingCancellable {
val mangaInfo = mangaInput.getMangaInfo()
if (mangaInfo != null && mangaInfo.id == remoteManga.id) {
send(mangaInput)
storageManager.getReadableDirs().forEach { dir ->
dir.forEachDirectoryEntry { entry ->
launch {
val mangaInput = LocalMangaInput.of(entry.toFile())
runCatchingCancellable {
val mangaInfo = mangaInput.getMangaInfo()
if (mangaInfo != null && mangaInfo.id == remoteManga.id) {
send(mangaInput)
}
}.onFailure {
it.printStackTraceDebug()
}
}.onFailure {
it.printStackTraceDebug()
}
}
}
@ -171,10 +169,10 @@ class LocalMangaRepository @Inject constructor(
}
val dirs = storageManager.getWriteableDirs()
runInterruptible(Dispatchers.IO) {
dirs.flatMap { dir ->
dir.listFiles(TempFileFilter())?.toList().orEmpty()
}.forEach { file ->
file.deleteRecursively()
dirs.forEach { dir ->
dir.toPath().forEachDirectoryEntry("*.tmp") {
it.deleteExisting()
}
}
}
return true
@ -188,20 +186,21 @@ class LocalMangaRepository @Inject constructor(
locks.unlock(id)
}
private suspend fun getRawList(): ArrayList<LocalManga> {
val files = getAllFiles()
private suspend fun getRawList(): List<LocalManga> {
return coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
files.map { file ->
async(dispatcher) {
runCatchingCancellable { LocalMangaInput.ofOrNull(file)?.getManga() }.getOrNull()
storageManager.getReadableDirs().flatMap { dir ->
dir.useDirectoryEntries { entries ->
entries.map {
async(dispatcher) {
runCatchingCancellable { LocalMangaInput.ofOrNull(it)?.getManga() }.getOrNull()
}
}.toList() // Using toList() allows the directory stream to be closed.
}
}.awaitAll()
}.filterNotNullTo(ArrayList(files.size))
}
private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir ->
dir.listFiles()?.toList().orEmpty()
}
.awaitAll()
.filterNotNull()
}
}
private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }

@ -17,10 +17,11 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.io.File
import java.nio.file.Path
import javax.inject.Inject
import kotlin.io.path.isReadable
private const val DIR_NAME = "manga"
private const val NOMEDIA = ".nomedia"
@ -46,15 +47,15 @@ class LocalStorageManager @Inject constructor(
}
suspend fun computeCacheSize(cache: CacheDir) = withContext(Dispatchers.IO) {
getCacheDirs(cache.dir).sumOf { it.computeSize() }
getCacheDirs(cache.dir).sumOf { it.toPath().computeSize() }
}
suspend fun computeCacheSize() = withContext(Dispatchers.IO) {
getCacheDirs().sumOf { it.computeSize() }
getCacheDirs().sumOf { it.toPath().computeSize() }
}
suspend fun computeStorageSize() = withContext(Dispatchers.IO) {
getAvailableStorageDirs().sumOf { it.computeSize() }
getAvailableStorageDirs().sumOf { it.toPath().computeSize() }
}
suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) {
@ -65,19 +66,20 @@ class LocalStorageManager @Inject constructor(
getCacheDirs(cache.dir).forEach { it.deleteRecursively() }
}
suspend fun getReadableDirs(): List<File> = runInterruptible(Dispatchers.IO) {
suspend fun getReadableDirs(): List<Path> = runInterruptible(Dispatchers.IO) {
getConfiguredStorageDirs()
.map { it.toPath() }
.filter { it.isReadable() }
}
suspend fun getWriteableDirs(): List<File> = runInterruptible(Dispatchers.IO) {
getConfiguredStorageDirs()
.filter { it.isWriteable() }
.filter { it.isWritable() }
}
suspend fun getDefaultWriteableDir(): File? = runInterruptible(Dispatchers.IO) {
val preferredDir = settings.mangaStorageDir?.takeIf { it.isWriteable() }
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
val preferredDir = settings.mangaStorageDir?.takeIf { it.isWritable() }
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWritable() }
}
suspend fun getApplicationStorageDirs(): Set<File> = runInterruptible(Dispatchers.IO) {
@ -146,7 +148,7 @@ class LocalStorageManager @Inject constructor(
}
@WorkerThread
private fun getCacheDirs(): MutableSet<File> {
private fun getCacheDirs(): Set<File> {
val result = LinkedHashSet<File>()
result += context.cacheDir
context.externalCacheDirs.filterNotNullTo(result)
@ -163,11 +165,7 @@ class LocalStorageManager @Inject constructor(
}
}
private fun File.isReadable() = runCatching {
canRead()
}.getOrDefault(false)
private fun File.isWriteable() = runCatching {
private fun File.isWritable() = runCatching {
canWrite()
}.getOrDefault(false)
}

@ -4,6 +4,7 @@ import androidx.annotation.WorkerThread
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
@ -126,8 +127,7 @@ class MangaIndex(source: String?) {
item.put("id", id)
list.add(item)
}
val comparator = org.koitharu.kotatsu.core.util.AlphanumComparator()
list.sortWith(compareBy(comparator) { it.getString("name") })
list.sortWith(compareBy(AlphanumComparator()) { it.getString("name") })
val newJo = JSONObject()
list.forEachIndexed { i, obj ->
obj.put("number", i + 1)

@ -1,11 +0,0 @@
package org.koitharu.kotatsu.local.data
import java.io.File
import java.io.FilenameFilter
class TempFileFilter : FilenameFilter {
override fun accept(dir: File, name: String): Boolean {
return name.endsWith(".tmp", ignoreCase = true)
}
}

@ -15,9 +15,9 @@ import okio.source
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.util.ext.resolveName
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.hasCbzExtension
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.domain.model.LocalManga
import java.io.File
@ -46,7 +46,7 @@ class SingleMangaImporter @Inject constructor(
private suspend fun importFile(uri: Uri): LocalManga = withContext(Dispatchers.IO) {
val contentResolver = storageManager.contentResolver
val name = contentResolver.resolveName(uri) ?: throw IOException("Cannot fetch name from uri: $uri")
if (!CbzFilter.isFileSupported(name)) {
if (!hasCbzExtension(name)) {
throw UnsupportedFileException("Unsupported file on $uri")
}
val dest = File(getOutputDir(), name)

@ -4,12 +4,13 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.ext.listFilesRecursive
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.toListSorted
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.ImageFileFilter
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.hasImageExtension
import org.koitharu.kotatsu.local.data.isCbzExtension
import org.koitharu.kotatsu.local.data.isImageExtension
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
@ -18,7 +19,14 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toCamelCase
import java.io.File
import java.nio.file.Path
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.extension
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.name
import kotlin.io.path.nameWithoutExtension
import kotlin.io.path.walk
/**
* Manga {Folder}
@ -61,7 +69,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
name = f.nameWithoutExtension.toHumanReadable(),
number = i + 1,
source = MangaSource.LOCAL,
uploadDate = f.lastModified(),
uploadDate = f.getLastModifiedTime().toMillis(),
url = f.toUri().toString(),
scanlator = null,
branch = null,
@ -84,11 +92,13 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
index?.getMangaInfo()
}
@OptIn(ExperimentalPathApi::class)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
val file = chapter.url.toUri().toFile()
if (file.isDirectory) {
file.listFilesRecursive(ImageFileFilter())
.toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
file.toPath().walk()
.filter { isImageExtension(it.extension) }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map {
val pageUri = it.toUri().toString()
MangaPage(
@ -104,7 +114,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
.asSequence()
.filter { x -> !x.isDirectory }
.map { it.name }
.toListSorted(org.koitharu.kotatsu.core.util.AlphanumComparator())
.toListSorted(AlphanumComparator())
.map {
val pageUri = zipUri(file, it)
MangaPage(
@ -120,20 +130,25 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
private fun getChaptersFiles(): List<File> = root.listFilesRecursive(CbzFilter())
.toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
@OptIn(ExperimentalPathApi::class)
private fun getChaptersFiles(): List<Path> = root.toPath().walk()
.filter { isCbzExtension(it.extension) }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
@OptIn(ExperimentalPathApi::class)
private fun findFirstImageEntry(): String? {
val filter = ImageFileFilter()
root.listFilesRecursive(filter).firstOrNull()?.let {
return it.toUri().toString()
}
val cbz = root.listFilesRecursive(CbzFilter()).firstOrNull() ?: return null
return ZipFile(cbz).use { zip ->
zip.entries().asSequence()
.firstOrNull { x -> !x.isDirectory && filter.accept(x) }
?.let { entry -> zipUri(cbz, entry.name) }
}
val rootPath = root.toPath()
return rootPath.walk()
.filter { isImageExtension(it.extension) }
.firstOrNull()?.toUri()?.toString()
?: run {
val cbz = rootPath.walk().filter { isCbzExtension(it.extension) }.firstOrNull()?.toFile() ?: return null
return ZipFile(cbz).use { zip ->
zip.entries().asSequence()
.firstOrNull { x -> !x.isDirectory && hasImageExtension(x) }
?.let { entry -> zipUri(cbz, entry.name) }
}
}
}
private fun fileUri(base: File, name: String): String {

@ -2,13 +2,16 @@ package org.koitharu.kotatsu.local.data.input
import android.net.Uri
import androidx.core.net.toFile
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.isCbzExtension
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.io.File
import java.nio.file.Path
import kotlin.io.path.extension
import kotlin.io.path.isDirectory
sealed class LocalMangaInput(
protected val root: File,
@ -31,9 +34,9 @@ sealed class LocalMangaInput(
else -> LocalMangaZipInput(file)
}
fun ofOrNull(file: File): LocalMangaInput? = when {
file.isDirectory -> LocalMangaDirInput(file)
CbzFilter.isFileSupported(file.name) -> LocalMangaZipInput(file)
fun ofOrNull(path: Path): LocalMangaInput? = when {
path.isDirectory() -> LocalMangaDirInput(path.toFile())
isCbzExtension(path.extension) -> LocalMangaZipInput(path.toFile())
else -> null
}

@ -7,6 +7,7 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.readText
import org.koitharu.kotatsu.core.util.ext.toListSorted
@ -70,7 +71,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
publicUrl = fileUri,
source = MangaSource.LOCAL,
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
chapters = chapters.sortedWith(org.koitharu.kotatsu.core.util.AlphanumComparator())
chapters = chapters.sortedWith(AlphanumComparator())
.mapIndexed { i, s ->
MangaChapter(
id = "$i$s".longHashCode(),
@ -125,7 +126,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
}
}
entries
.toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map { x ->
val entryUri = zipUri(file, x.name)
MangaPage(
@ -141,7 +142,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
private fun findFirstImageEntry(entries: Enumeration<out ZipEntry>): ZipEntry? {
val list = entries.toList()
.filterNot { it.isDirectory }
.sortedWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
val map = MimeTypeMap.getSingleton()
return list.firstOrNull {
map.getMimeTypeFromExtension(it.name.substringAfterLast('.'))

@ -41,8 +41,8 @@ import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
import org.koitharu.kotatsu.core.zip.ZipPool
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.hasCbzExtension
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
@ -198,7 +198,7 @@ class PageLoader @Inject constructor(
val pageUrl = getPageUrl(page)
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
val uri = Uri.parse(pageUrl)
return if (CbzFilter.isUriSupported(uri)) {
return if (hasCbzExtension(uri)) {
runInterruptible(Dispatchers.IO) {
zipPool[uri]
}.use {

@ -19,8 +19,8 @@ import okio.source
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.hasCbzExtension
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.mimeType
@ -56,7 +56,7 @@ class MangaPageFetcher(
private suspend fun loadPage(pageUrl: String): SourceResult {
val uri = pageUrl.toUri()
return if (CbzFilter.isUriSupported(uri)) {
return if (hasCbzExtension(uri)) {
val zip = runInterruptible(Dispatchers.IO) { ZipFile(uri.schemeSpecificPart) }
val entry = runInterruptible(Dispatchers.IO) { zip.getEntry(uri.fragment) }
return SourceResult(

Loading…
Cancel
Save