Fix local manga directories chapters
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user