Fix local manga directories chapters
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 625
|
versionCode = 626
|
||||||
versionName = '6.7.3'
|
versionName = '6.7.4'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import java.nio.file.attribute.BasicFileAttributes
|
|||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import kotlin.io.path.ExperimentalPathApi
|
import kotlin.io.path.ExperimentalPathApi
|
||||||
|
import kotlin.io.path.PathWalkOption
|
||||||
import kotlin.io.path.readAttributes
|
import kotlin.io.path.readAttributes
|
||||||
import kotlin.io.path.walk
|
import kotlin.io.path.walk
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ fun ContentResolver.resolveName(uri: Uri): String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
|
suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
|
||||||
walkCompat().sumOf { it.length() }
|
walkCompat(includeDirectories = false).sumOf { it.length() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun File.children() = FileSequence(this)
|
fun File.children() = FileSequence(this)
|
||||||
@@ -87,10 +88,16 @@ val File.creationTime
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPathApi::class)
|
@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
|
// 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 {
|
} else {
|
||||||
// Directories are excluded by default in Path.walk(), so do it here as well
|
// 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 {
|
fun Uri.isZipUri() = scheme.let {
|
||||||
it == URI_SCHEME_ZIP || it == "cbz" || it == "zip"
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
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.creationTime
|
||||||
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||||
import org.koitharu.kotatsu.core.util.ext.toListSorted
|
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) {
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
|
||||||
val file = chapter.url.toUri().toFile()
|
val file = chapter.url.toUri().toFile()
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
file.walkCompat()
|
file.children()
|
||||||
.filter { hasImageExtension(it) }
|
.filter { it.isFile && hasImageExtension(it) }
|
||||||
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
|
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
|
||||||
.map {
|
.map {
|
||||||
val pageUri = it.toUri().toString()
|
val pageUri = it.toUri().toString()
|
||||||
@@ -129,14 +130,16 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
|
|
||||||
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
|
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
|
||||||
|
|
||||||
private fun getChaptersFiles() = root.walkCompat()
|
private fun getChaptersFiles() = root.walkCompat(includeDirectories = true)
|
||||||
.filter { it.hasCbzExtension() }
|
.filter { it != root && it.isChapterDirectory() || it.hasCbzExtension() }
|
||||||
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
|
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
|
||||||
|
|
||||||
private fun findFirstImageEntry(): String? {
|
private fun findFirstImageEntry(): String? {
|
||||||
return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
|
return root.walkCompat(includeDirectories = false)
|
||||||
|
.firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
|
||||||
?: run {
|
?: 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 ->
|
ZipFile(cbz).use { zip ->
|
||||||
zip.entries().asSequence()
|
zip.entries().asSequence()
|
||||||
.firstOrNull { !it.isDirectory && hasImageExtension(it.name) }
|
.firstOrNull { !it.isDirectory && hasImageExtension(it.name) }
|
||||||
@@ -148,4 +151,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
private fun fileUri(base: File, name: String): String {
|
private fun fileUri(base: File, name: String): String {
|
||||||
return File(base, name).toUri().toString()
|
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.ext.withProgress
|
||||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
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.isZipUri
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -203,20 +204,23 @@ class PageLoader @Inject constructor(
|
|||||||
val pageUrl = getPageUrl(page)
|
val pageUrl = getPageUrl(page)
|
||||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" }
|
check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" }
|
||||||
val uri = Uri.parse(pageUrl)
|
val uri = Uri.parse(pageUrl)
|
||||||
return if (uri.isZipUri()) {
|
return when {
|
||||||
if (uri.scheme == URI_SCHEME_ZIP) {
|
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {
|
||||||
uri
|
uri
|
||||||
} else { // legacy uri
|
} else { // legacy uri
|
||||||
uri.buildUpon().scheme(URI_SCHEME_ZIP).build()
|
uri.buildUpon().scheme(URI_SCHEME_ZIP).build()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val request = createPageRequest(page, pageUrl)
|
uri.isFileUri() -> uri
|
||||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
else -> {
|
||||||
val body = checkNotNull(response.body) { "Null response body" }
|
val request = createPageRequest(page, pageUrl)
|
||||||
body.withProgress(progress).use {
|
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||||
cache.put(pageUrl, it.source())
|
val body = checkNotNull(response.body) { "Null response body" }
|
||||||
}
|
body.withProgress(progress).use {
|
||||||
}.toUri()
|
cache.put(pageUrl, it.source())
|
||||||
|
}
|
||||||
|
}.toUri()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
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.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
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.isZipUri
|
||||||
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
@@ -56,8 +59,8 @@ class MangaPageFetcher(
|
|||||||
|
|
||||||
private suspend fun loadPage(pageUrl: String): SourceResult {
|
private suspend fun loadPage(pageUrl: String): SourceResult {
|
||||||
val uri = pageUrl.toUri()
|
val uri = pageUrl.toUri()
|
||||||
return if (uri.isZipUri()) {
|
return when {
|
||||||
runInterruptible(Dispatchers.IO) {
|
uri.isZipUri() -> runInterruptible(Dispatchers.IO) {
|
||||||
val zip = ZipFile(uri.schemeSpecificPart)
|
val zip = ZipFile(uri.schemeSpecificPart)
|
||||||
val entry = zip.getEntry(uri.fragment)
|
val entry = zip.getEntry(uri.fragment)
|
||||||
SourceResult(
|
SourceResult(
|
||||||
@@ -66,32 +69,48 @@ class MangaPageFetcher(
|
|||||||
context = context,
|
context = context,
|
||||||
metadata = MangaPageMetadata(page),
|
metadata = MangaPageMetadata(page),
|
||||||
),
|
),
|
||||||
mimeType = null,
|
mimeType = MimeTypeMap.getSingleton()
|
||||||
|
.getMimeTypeFromExtension(entry.name.substringAfterLast('.', "")),
|
||||||
dataSource = DataSource.DISK,
|
dataSource = DataSource.DISK,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val request = PageLoader.createPageRequest(page, pageUrl)
|
uri.isFileUri() -> runInterruptible(Dispatchers.IO) {
|
||||||
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
|
val file = uri.toFile()
|
||||||
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(
|
SourceResult(
|
||||||
source = ImageSource(
|
source = ImageSource(
|
||||||
file = file.toOkioPath(),
|
source = file.source().buffer(),
|
||||||
|
context = context,
|
||||||
metadata = MangaPageMetadata(page),
|
metadata = MangaPageMetadata(page),
|
||||||
),
|
),
|
||||||
mimeType = mimeType,
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension),
|
||||||
dataSource = DataSource.NETWORK,
|
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