Fix local manga directories chapters

This commit is contained in:
Koitharu
2024-02-28 16:12:49 +02:00
parent 9f3113363b
commit f8cbc9692f
6 changed files with 80 additions and 41 deletions

View File

@@ -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 {

View File

@@ -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<File> = 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 }
}

View File

@@ -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"

View File

@@ -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<MangaPage> = 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) }
}
}

View File

@@ -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()
}
}
}

View File

@@ -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,
)
}
}
}
}