Support nested cbz covers

This commit is contained in:
Koitharu
2025-01-19 16:26:00 +02:00
parent 169539f42f
commit 3a42dce45f
2 changed files with 54 additions and 33 deletions

View File

@@ -23,8 +23,9 @@ class CbzFetcher(
override suspend fun fetch() = runInterruptible { override suspend fun fetch() = runInterruptible {
val filePath = uri.schemeSpecificPart.toPath() val filePath = uri.schemeSpecificPart.toPath()
val entryName = requireNotNull(uri.fragment) val entryName = requireNotNull(uri.fragment)
val fs = options.fileSystem.openZip(filePath)
SourceFetchResult( SourceFetchResult(
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)), source = ImageSource(entryName.toPath(), fs, closeable = fs),
mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(), mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(),
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )

View File

@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.MimeTypes import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.isDirectory
import org.koitharu.kotatsu.core.util.ext.isFileUri import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isImage import org.koitharu.kotatsu.core.util.ext.isImage
import org.koitharu.kotatsu.core.util.ext.isRegularFile import org.koitharu.kotatsu.core.util.ext.isRegularFile
@@ -59,12 +60,13 @@ class LocalMangaParser(private val uri: Uri) {
val index = MangaIndex.read(fileSystem, rootPath / ENTRY_NAME_INDEX) val index = MangaIndex.read(fileSystem, rootPath / ENTRY_NAME_INDEX)
val mangaInfo = index?.getMangaInfo() val mangaInfo = index?.getMangaInfo()
if (mangaInfo != null) { if (mangaInfo != null) {
val coverEntry: Path? = val coverEntry: Path? = index.getCoverEntry()?.let { rootPath / it }
index.getCoverEntry()?.let { rootPath / it } ?: fileSystem.findFirstImage(rootPath)
mangaInfo.copy( mangaInfo.copy(
source = LocalMangaSource, source = LocalMangaSource,
url = rootFile.toUri().toString(), url = rootFile.toUri().toString(),
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() }, coverUrl = coverEntry?.let {
uri.child(it, resolve = true).toString()
} ?: fileSystem.findFirstImageUri(rootPath)?.toString(),
largeCoverUrl = null, largeCoverUrl = null,
chapters = if (withDetails) { chapters = if (withDetails) {
mangaInfo.chapters?.mapNotNull { c -> mangaInfo.chapters?.mapNotNull { c ->
@@ -86,19 +88,17 @@ class LocalMangaParser(private val uri: Uri) {
) )
} else { } else {
val title = rootFile.name.fileNameToTitle() val title = rootFile.name.fileNameToTitle()
val coverEntry = fileSystem.findFirstImage(rootPath)
Manga( Manga(
id = rootFile.absolutePath.longHashCode(), id = rootFile.absolutePath.longHashCode(),
title = title, title = title,
url = rootFile.toUri().toString(), url = rootFile.toUri().toString(),
publicUrl = rootFile.toUri().toString(), publicUrl = rootFile.toUri().toString(),
source = LocalMangaSource, source = LocalMangaSource,
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() }, coverUrl = fileSystem.findFirstImageUri(rootPath)?.toString(),
chapters = if (withDetails) { chapters = if (withDetails) {
val chapters = fileSystem.listRecursively(rootPath) val chapters = fileSystem.listRecursively(rootPath)
.mapNotNullTo(HashSet()) { path -> .mapNotNullTo(HashSet()) { path ->
when { when {
path == coverEntry -> null
!fileSystem.isRegularFile(path) -> null !fileSystem.isRegularFile(path) -> null
path.isImage() -> path.parent path.isImage() -> path.parent
hasZipExtension(path.name) -> path hasZipExtension(path.name) -> path
@@ -171,21 +171,55 @@ class LocalMangaParser(private val uri: Uri) {
} }
private fun Uri.child(path: Path, resolve: Boolean): Uri { private fun Uri.child(path: Path, resolve: Boolean): Uri {
val file = toFile()
val builder = buildUpon() val builder = buildUpon()
if (isZipUri() || !resolve) { val isZip = isZipUri() || file.isZipArchive
if (isZip) {
builder.scheme(URI_SCHEME_ZIP)
}
if (isZip || !resolve) {
builder.fragment(path.toString().removePrefix(Path.DIRECTORY_SEPARATOR)) builder.fragment(path.toString().removePrefix(Path.DIRECTORY_SEPARATOR))
} else { } else {
val file = toFile() builder.appendEncodedPath(path.relativeTo(file.toOkioPath()).toString())
if (file.isZipArchive) {
builder.fragment(path.toString().removePrefix(Path.DIRECTORY_SEPARATOR))
builder.scheme(URI_SCHEME_ZIP)
} else {
builder.appendEncodedPath(path.relativeTo(file.toOkioPath()).toString())
}
} }
return builder.build() return builder.build()
} }
private fun FileSystem.findFirstImageUri(
rootPath: Path,
recursive: Boolean = false
): Uri? = runCatchingCancellable {
val list = list(rootPath)
for (file in list.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })) {
if (isRegularFile(file)) {
if (file.isImage()) {
return@runCatchingCancellable uri.child(file, resolve = true)
}
if (recursive && file.isZip()) {
openZip(file).use { zipFs ->
zipFs.findFirstImageUri(Path.DIRECTORY_SEPARATOR.toPath())?.let { subUri ->
val subPath = subUri.path.orEmpty().removePrefix(uri.path.orEmpty())
.replace(REGEX_PARENT_PATH_PREFIX, "")
return@runCatchingCancellable uri.child(file, resolve = true)
.child(subPath.toPath(), resolve = false)
}
}
}
} else if (recursive && isDirectory(file)) {
findFirstImageUri(file, true)?.let {
return@runCatchingCancellable it
}
}
}
if (recursive) {
null
} else {
findFirstImageUri(rootPath, recursive = true)
}
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrNull()
private class FsAndPath( private class FsAndPath(
val fileSystem: FileSystem, val fileSystem: FileSystem,
val path: Path, val path: Path,
@@ -205,6 +239,8 @@ class LocalMangaParser(private val uri: Uri) {
companion object { companion object {
private val REGEX_PARENT_PATH_PREFIX = Regex("^(/\\.\\.)+")
@Blocking @Blocking
fun getOrNull(file: File): LocalMangaParser? = if ((file.isDirectory || file.isZipArchive) && file.canRead()) { fun getOrNull(file: File): LocalMangaParser? = if ((file.isDirectory || file.isZipArchive) && file.canRead()) {
LocalMangaParser(file) LocalMangaParser(file)
@@ -225,26 +261,10 @@ class LocalMangaParser(private val uri: Uri) {
} }
}.flowOn(Dispatchers.Default).firstOrNull() }.flowOn(Dispatchers.Default).firstOrNull()
private fun FileSystem.findFirstImage(rootPath: Path) = findFirstImageImpl(rootPath, false)
?: findFirstImageImpl(rootPath, true)
private fun FileSystem.findFirstImageImpl(
rootPath: Path,
recursive: Boolean
): Path? = runCatchingCancellable {
if (recursive) {
listRecursively(rootPath)
} else {
list(rootPath).asSequence()
}.filter { isRegularFile(it) && it.isImage() }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() })
.firstOrNull()
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrNull()
private fun Path.isImage(): Boolean = MimeTypes.getMimeTypeFromExtension(name)?.isImage == true private fun Path.isImage(): Boolean = MimeTypes.getMimeTypeFromExtension(name)?.isImage == true
private fun Path.isZip(): Boolean = hasZipExtension(name)
private fun Uri.resolve(): Uri = if (isFileUri()) { private fun Uri.resolve(): Uri = if (isFileUri()) {
val file = toFile() val file = toFile()
if (file.isZipArchive) { if (file.isZipArchive) {