From f8cbc9692f2ffb9cc1b8301d62c28f6fd94ec009 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 28 Feb 2024 16:12:49 +0200 Subject: [PATCH] Fix local manga directories chapters --- app/build.gradle | 4 +- .../koitharu/kotatsu/core/util/ext/File.kt | 15 +++-- .../koitharu/kotatsu/local/data/CbzFilter.kt | 2 + .../local/data/input/LocalMangaDirInput.kt | 19 +++++-- .../kotatsu/reader/domain/PageLoader.kt | 24 ++++---- .../reader/ui/thumbnails/MangaPageFetcher.kt | 57 ++++++++++++------- 6 files changed, 80 insertions(+), 41 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fad31342c..b65af9ece 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 34 - versionCode = 625 - versionName = '6.7.3' + versionCode = 626 + versionName = '6.7.4' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index 77afae296..63518ad17 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -19,6 +19,7 @@ import java.nio.file.attribute.BasicFileAttributes import java.util.zip.ZipEntry import java.util.zip.ZipFile import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.PathWalkOption import kotlin.io.path.readAttributes import kotlin.io.path.walk @@ -72,7 +73,7 @@ fun ContentResolver.resolveName(uri: Uri): String? { } suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) { - walkCompat().sumOf { it.length() } + walkCompat(includeDirectories = false).sumOf { it.length() } } fun File.children() = FileSequence(this) @@ -87,10 +88,16 @@ val File.creationTime } @OptIn(ExperimentalPathApi::class) -fun File.walkCompat() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +fun File.walkCompat(includeDirectories: Boolean): Sequence = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Use lazy loading on Android 8.0 and later - toPath().walk().map { it.toFile() } + val walk = if (includeDirectories) { + toPath().walk(PathWalkOption.INCLUDE_DIRECTORIES) + } else { + toPath().walk() + } + walk.map { it.toFile() } } else { // Directories are excluded by default in Path.walk(), so do it here as well - walk().filter { it.isFile } + val walk = walk() + if (includeDirectories) walk else walk.filter { it.isFile } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt index b9f2eebb4..3539e8ca7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt @@ -18,3 +18,5 @@ fun File.hasCbzExtension() = isCbzExtension(extension) fun Uri.isZipUri() = scheme.let { it == URI_SCHEME_ZIP || it == "cbz" || it == "zip" } + +fun Uri.isFileUri() = scheme == "file" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt index dcf416efc..3781a7c4e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt @@ -5,6 +5,7 @@ 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.children import org.koitharu.kotatsu.core.util.ext.creationTime import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.toListSorted @@ -100,8 +101,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { override suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { - file.walkCompat() - .filter { hasImageExtension(it) } + file.children() + .filter { it.isFile && hasImageExtension(it) } .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) .map { val pageUri = it.toUri().toString() @@ -129,14 +130,16 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { private fun String.toHumanReadable() = replace("_", " ").toCamelCase() - private fun getChaptersFiles() = root.walkCompat() - .filter { it.hasCbzExtension() } + private fun getChaptersFiles() = root.walkCompat(includeDirectories = true) + .filter { it != root && it.isChapterDirectory() || it.hasCbzExtension() } .associateByTo(TreeMap(AlphanumComparator())) { it.name } private fun findFirstImageEntry(): String? { - return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString() + return root.walkCompat(includeDirectories = false) + .firstOrNull { hasImageExtension(it) }?.toUri()?.toString() ?: run { - val cbz = root.walkCompat().firstOrNull { it.hasCbzExtension() } ?: return null + val cbz = root.walkCompat(includeDirectories = false) + .firstOrNull { it.hasCbzExtension() } ?: return null ZipFile(cbz).use { zip -> zip.entries().asSequence() .firstOrNull { !it.isDirectory && hasImageExtension(it.name) } @@ -148,4 +151,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { private fun fileUri(base: File, name: String): String { return File(base, name).toUri().toString() } + + private fun File.isChapterDirectory(): Boolean { + return isDirectory && children().any { hasImageExtension(it) } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 42e5ee65a..013b0e883 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -47,6 +47,7 @@ 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.local.data.PagesCache +import org.koitharu.kotatsu.local.data.isFileUri import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource @@ -203,20 +204,23 @@ class PageLoader @Inject constructor( val pageUrl = getPageUrl(page) check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" } val uri = Uri.parse(pageUrl) - return if (uri.isZipUri()) { - if (uri.scheme == URI_SCHEME_ZIP) { + return when { + uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) { uri } else { // legacy uri uri.buildUpon().scheme(URI_SCHEME_ZIP).build() } - } else { - val request = createPageRequest(page, pageUrl) - imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> - val body = checkNotNull(response.body) { "Null response body" } - body.withProgress(progress).use { - cache.put(pageUrl, it.source()) - } - }.toUri() + + uri.isFileUri() -> uri + else -> { + val request = createPageRequest(page, pageUrl) + imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> + val body = checkNotNull(response.body) { "Null response body" } + body.withProgress(progress).use { + cache.put(pageUrl, it.source()) + } + }.toUri() + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt index 2861cd870..0a693d701 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt @@ -1,6 +1,8 @@ package org.koitharu.kotatsu.reader.ui.thumbnails import android.content.Context +import android.webkit.MimeTypeMap +import androidx.core.net.toFile import androidx.core.net.toUri import coil.ImageLoader import coil.decode.DataSource @@ -20,6 +22,7 @@ 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.PagesCache +import org.koitharu.kotatsu.local.data.isFileUri import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.local.data.util.withExtraCloseable import org.koitharu.kotatsu.parsers.model.MangaPage @@ -56,8 +59,8 @@ class MangaPageFetcher( private suspend fun loadPage(pageUrl: String): SourceResult { val uri = pageUrl.toUri() - return if (uri.isZipUri()) { - runInterruptible(Dispatchers.IO) { + return when { + uri.isZipUri() -> runInterruptible(Dispatchers.IO) { val zip = ZipFile(uri.schemeSpecificPart) val entry = zip.getEntry(uri.fragment) SourceResult( @@ -66,32 +69,48 @@ class MangaPageFetcher( context = context, metadata = MangaPageMetadata(page), ), - mimeType = null, + mimeType = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(entry.name.substringAfterLast('.', "")), dataSource = DataSource.DISK, ) } - } else { - val request = PageLoader.createPageRequest(page, pageUrl) - imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response -> - check(response.isSuccessful) { - "Invalid response: ${response.code} ${response.message} at $pageUrl" - } - val body = checkNotNull(response.body) { - "Null response" - } - val mimeType = response.mimeType - val file = body.use { - pagesCache.put(pageUrl, it.source()) - } + + uri.isFileUri() -> runInterruptible(Dispatchers.IO) { + val file = uri.toFile() SourceResult( source = ImageSource( - file = file.toOkioPath(), + source = file.source().buffer(), + context = context, metadata = MangaPageMetadata(page), ), - mimeType = mimeType, - dataSource = DataSource.NETWORK, + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension), + dataSource = DataSource.DISK, ) } + + else -> { + val request = PageLoader.createPageRequest(page, pageUrl) + imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response -> + check(response.isSuccessful) { + "Invalid response: ${response.code} ${response.message} at $pageUrl" + } + val body = checkNotNull(response.body) { + "Null response" + } + val mimeType = response.mimeType + val file = body.use { + pagesCache.put(pageUrl, it.source()) + } + SourceResult( + source = ImageSource( + file = file.toOkioPath(), + metadata = MangaPageMetadata(page), + ), + mimeType = mimeType, + dataSource = DataSource.NETWORK, + ) + } + } } }