Support nested cbz covers
This commit is contained in:
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user