From 9425d295961e11b12b45f739003a5f77e2ea761b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 26 Oct 2024 08:37:40 +0300 Subject: [PATCH] Migrate LocalMangaInfo to Okio --- app/build.gradle | 2 +- .../core/parser/favicon/FaviconFetcher.kt | 8 +- .../org/koitharu/kotatsu/core/util/ext/IO.kt | 15 + .../org/koitharu/kotatsu/core/util/ext/Uri.kt | 15 +- .../download/ui/worker/DownloadWorker.kt | 6 +- .../koitharu/kotatsu/local/data/CbzFilter.kt | 9 +- .../local/data/LocalMangaRepository.kt | 29 +- .../koitharu/kotatsu/local/data/MangaIndex.kt | 31 +- .../data/importer/SingleMangaImporter.kt | 10 +- .../local/data/index/LocalMangaIndex.kt | 6 +- .../local/data/input/LocalMangaDirInput.kt | 159 --------- .../local/data/input/LocalMangaInput.kt | 111 ------- .../local/data/input/LocalMangaParser.kt | 309 ++++++++++++++++++ .../local/data/input/LocalMangaZipInput.kt | 155 --------- .../local/data/output/LocalMangaDirOutput.kt | 4 +- .../local/data/output/LocalMangaOutput.kt | 4 +- .../local/domain/LocalObserveMapper.kt | 2 +- .../kotatsu/local/domain/model/LocalManga.kt | 3 + .../kotatsu/reader/domain/PageLoader.kt | 4 - 19 files changed, 408 insertions(+), 474 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaParser.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt diff --git a/app/build.gradle b/app/build.gradle index ea0be15f9..0c1561411 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:3d5cc5ceff') { + implementation('com.github.KotatsuApp:kotatsu-parsers:1.4') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt index fb7753d9b..a47b03050 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt @@ -19,6 +19,7 @@ import coil3.toAndroidUri import kotlinx.coroutines.ensureActive import kotlinx.coroutines.runInterruptible import okio.IOException +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.parser.EmptyMangaRepository @@ -26,6 +27,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository import org.koitharu.kotatsu.core.util.ext.fetch +import org.koitharu.kotatsu.local.data.LocalMangaRepository import kotlin.coroutines.coroutineContext import coil3.Uri as CoilUri @@ -36,7 +38,7 @@ class FaviconFetcher( private val mangaRepositoryFactory: MangaRepository.Factory, ) : Fetcher { - override suspend fun fetch(): FetchResult { + override suspend fun fetch(): FetchResult? { val mangaSource = MangaSource(uri.schemeSpecificPart) return when (val repo = mangaRepositoryFactory.create(mangaSource)) { @@ -48,7 +50,9 @@ class FaviconFetcher( dataSource = DataSource.MEMORY, ) - else -> throw IllegalArgumentException("") + is LocalMangaRepository -> imageLoader.fetch(R.drawable.ic_storage, options) + + else -> throw IllegalArgumentException("Unsupported repo ${repo.javaClass.simpleName}") } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt index 41cf24f06..8cce14a33 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt @@ -7,6 +7,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext import okhttp3.ResponseBody import okio.BufferedSink +import okio.FileSystem +import okio.IOException +import okio.Path import okio.Source import org.koitharu.kotatsu.core.util.CancellableSource import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody @@ -33,3 +36,15 @@ fun InputStream.toByteBuffer(): ByteBuffer { val bytes = outStream.toByteArray() return ByteBuffer.allocateDirect(bytes.size).put(bytes).position(0) as ByteBuffer } + +fun FileSystem.isDirectory(path: Path) = try { + metadataOrNull(path)?.isDirectory == true +} catch (_: IOException) { + false +} + +fun FileSystem.isRegularFile(path: Path) = try { + metadataOrNull(path)?.isRegularFile == true +} catch (_: IOException) { + false +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt index d03fb6617..4bd885bef 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt @@ -1,6 +1,8 @@ package org.koitharu.kotatsu.core.util.ext import android.net.Uri +import androidx.core.net.toUri +import okio.Path import java.io.File const val URI_SCHEME_ZIP = "file+zip" @@ -20,6 +22,17 @@ fun Uri.isNetworkUri() = scheme.let { it == URI_SCHEME_HTTP || it == URI_SCHEME_HTTPS } -fun File.toZipUri(entryName: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryName") +fun File.toZipUri(entryPath: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryPath") + +fun File.toZipUri(entryPath: Path?): Uri = + toZipUri(entryPath?.toString()?.removePrefix(Path.DIRECTORY_SEPARATOR).orEmpty()) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) + +fun File.toUri(fragment: String?): Uri = toUri().run { + if (fragment != null) { + buildUpon().fragment(fragment).build() + } else { + this + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 58f0a8d39..ad70e34e4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -71,7 +71,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.TempFileFilter -import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.domain.MangaLock import org.koitharu.kotatsu.local.domain.model.LocalManga @@ -262,7 +262,7 @@ class DownloadWorker @AssistedInject constructor( } if (output.flushChapter(chapter.value)) { runCatchingCancellable { - localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga()) + localStorageChanges.emit(LocalMangaParser(output.rootFile).getManga(withDetails = false)) }.onFailure(Throwable::printStackTraceDebug) } publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1)) @@ -270,7 +270,7 @@ class DownloadWorker @AssistedInject constructor( publishState(currentState.copy(isIndeterminate = true, eta = -1L, isStuck = false)) output.mergeWithExisting() output.finish() - val localManga = LocalMangaInput.of(output.rootFile).getManga() + val localManga = LocalMangaParser(output.rootFile).getManga(withDetails = false) localStorageChanges.emit(localManga) publishState(currentState.copy(localManga = localManga, eta = -1L, isStuck = false)) } catch (e: Exception) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt index 0000dc536..3f1080d89 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt @@ -2,13 +2,14 @@ package org.koitharu.kotatsu.local.data import java.io.File -private fun isCbzExtension(ext: String?): Boolean { +private fun isZipExtension(ext: String?): Boolean { return ext.equals("cbz", ignoreCase = true) || ext.equals("zip", ignoreCase = true) } -fun hasCbzExtension(string: String): Boolean { +fun hasZipExtension(string: String): Boolean { val ext = string.substringAfterLast('.', "") - return isCbzExtension(ext) + return isZipExtension(ext) } -fun File.hasCbzExtension() = isCbzExtension(extension) +val File.isZipArchive: Boolean + get() = isFile && isZipExtension(extension) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index caa49998a..497967246 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data import android.net.Uri import androidx.core.net.toFile +import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -19,7 +20,7 @@ import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.withChildren import org.koitharu.kotatsu.local.data.index.LocalMangaIndex -import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.data.output.LocalMangaUtil import org.koitharu.kotatsu.local.domain.MangaLock @@ -125,15 +126,15 @@ class LocalMangaRepository @Inject constructor( } override suspend fun getDetails(manga: Manga): Manga = when { - !manga.isLocal -> requireNotNull(findSavedManga(manga)?.manga) { + !manga.isLocal -> requireNotNull(findSavedManga(manga, withDetails = true)?.manga) { "Manga is not local or saved" } - else -> LocalMangaInput.of(manga).getManga().manga + else -> LocalMangaParser(manga.url.toUri()).getManga(withDetails = true).manga } override suspend fun getPages(chapter: MangaChapter): List { - return LocalMangaInput.of(chapter).getPages(chapter) + return LocalMangaParser(chapter.url.toUri()).getPages(chapter) } suspend fun delete(manga: Manga): Boolean { @@ -147,7 +148,7 @@ class LocalMangaRepository @Inject constructor( } suspend fun deleteChapters(manga: Manga, ids: Set) = lock.withLock(manga) { - val subject = if (manga.isLocal) manga else checkNotNull(findSavedManga(manga)) { + val subject = if (manga.isLocal) manga else checkNotNull(findSavedManga(manga, withDetails = false)) { "Manga is not stored on local storage" }.manga LocalMangaUtil(subject).deleteChapters(ids) @@ -156,27 +157,27 @@ class LocalMangaRepository @Inject constructor( suspend fun getRemoteManga(localManga: Manga): Manga? { return runCatchingCancellable { - LocalMangaInput.of(localManga).getMangaInfo()?.takeUnless { it.isLocal } + LocalMangaParser(localManga.url.toUri()).getMangaInfo()?.takeUnless { it.isLocal } }.onFailure { it.printStackTraceDebug() }.getOrNull() } - suspend fun findSavedManga(remoteManga: Manga): LocalManga? = runCatchingCancellable { + suspend fun findSavedManga(remoteManga: Manga, withDetails: Boolean = true): LocalManga? = runCatchingCancellable { // very fast path - localMangaIndex.get(remoteManga.id)?.let { - return@runCatchingCancellable it + localMangaIndex.get(remoteManga.id, withDetails)?.let { cached -> + return@runCatchingCancellable cached } // fast path - LocalMangaInput.find(storageManager.getReadableDirs(), remoteManga)?.let { - return it.getManga() + LocalMangaParser.find(storageManager.getReadableDirs(), remoteManga)?.let { + return it.getManga(withDetails) } // slow path val files = getAllFiles() return channelFlow { for (file in files) { launch { - val mangaInput = LocalMangaInput.ofOrNull(file) + val mangaInput = LocalMangaParser.getOrNull(file) runCatchingCancellable { val mangaInfo = mangaInput?.getMangaInfo() if (mangaInfo != null && mangaInfo.id == remoteManga.id) { @@ -187,7 +188,7 @@ class LocalMangaRepository @Inject constructor( } } } - }.firstOrNull()?.getManga() + }.firstOrNull()?.getManga(withDetails) }.onSuccess { x: LocalManga? -> if (x != null) { localMangaIndex.put(x) @@ -237,7 +238,7 @@ class LocalMangaRepository @Inject constructor( for (file in files) { launch(dispatcher) { runCatchingCancellable { - LocalMangaInput.ofOrNull(file)?.getManga() + LocalMangaParser.getOrNull(file)?.getManga(withDetails = false) }.onFailure { e -> e.printStackTraceDebug() }.onSuccess { m -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt index 394746604..0ee9a3940 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt @@ -1,11 +1,17 @@ package org.koitharu.kotatsu.local.data import androidx.annotation.WorkerThread +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toOkioPath +import okio.buffer +import org.jetbrains.annotations.Blocking import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource @@ -18,6 +24,7 @@ import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.getStringOrNull import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.toTitleCase import java.io.File @@ -186,15 +193,25 @@ class MangaIndex(source: String?) { companion object { + @Blocking @WorkerThread - fun read(file: File): MangaIndex? { - if (file.exists() && file.canRead()) { - val text = file.readText() - if (text.length > 2) { - return MangaIndex(text) + fun read(fileSystem: FileSystem, path: Path): MangaIndex? = runCatchingCancellable { + val text = fileSystem.source(path).use { + it.buffer().use { buffer -> + buffer.readUtf8() } } - return null - } + if (text.length > 2) { + MangaIndex(text) + } else { + null + } + }.onFailure { e -> + e.printStackTraceDebug() + }.getOrNull() + + @Blocking + @WorkerThread + fun read(file: File): MangaIndex? = read(FileSystem.SYSTEM, file.toOkioPath()) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt index 88ad215e5..91eb31b7b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt @@ -17,8 +17,8 @@ import org.koitharu.kotatsu.core.util.ext.resolveName import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageManager -import org.koitharu.kotatsu.local.data.hasCbzExtension -import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.local.data.hasZipExtension +import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.domain.model.LocalManga import java.io.File import java.io.IOException @@ -46,7 +46,7 @@ class SingleMangaImporter @Inject constructor( private suspend fun importFile(uri: Uri): LocalManga = withContext(Dispatchers.IO) { val contentResolver = storageManager.contentResolver val name = contentResolver.resolveName(uri) ?: throw IOException("Cannot fetch name from uri: $uri") - if (!hasCbzExtension(name)) { + if (!hasZipExtension(name)) { throw UnsupportedFileException("Unsupported file $name on $uri") } val dest = File(getOutputDir(), name) @@ -57,7 +57,7 @@ class SingleMangaImporter @Inject constructor( output.writeAllCancellable(source.source()) } } ?: throw IOException("Cannot open input stream: $uri") - LocalMangaInput.of(dest).getManga() + LocalMangaParser(dest).getManga(withDetails = false) } private suspend fun importDirectory(uri: Uri): LocalManga { @@ -69,7 +69,7 @@ class SingleMangaImporter @Inject constructor( for (docFile in root.listFiles()) { docFile.copyTo(dest) } - return LocalMangaInput.of(dest).getManga() + return LocalMangaParser(dest).getManga(withDetails = false) } private suspend fun DocumentFile.copyTo(destDir: File) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt index 53caf3c53..d2425be9e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.local.data.LocalMangaRepository -import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import java.io.File @@ -57,7 +57,7 @@ class LocalMangaIndex @Inject constructor( } } - suspend fun get(mangaId: Long): LocalManga? { + suspend fun get(mangaId: Long, withDetails: Boolean): LocalManga? { updateIfRequired() var path = db.getLocalMangaIndexDao().findPath(mangaId) if (path == null && mutex.isLocked) { // wait for updating complete @@ -67,7 +67,7 @@ class LocalMangaIndex @Inject constructor( return null } return runCatchingCancellable { - LocalMangaInput.of(File(path)).getManga() + LocalMangaParser(File(path)).getManga(withDetails) }.onFailure { it.printStackTraceDebug() }.getOrNull() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt deleted file mode 100644 index 302e4a4e7..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt +++ /dev/null @@ -1,159 +0,0 @@ -package org.koitharu.kotatsu.local.data.input - -import androidx.core.net.toFile -import androidx.core.net.toUri -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible -import org.koitharu.kotatsu.core.model.LocalMangaSource -import org.koitharu.kotatsu.core.util.AlphanumComparator -import org.koitharu.kotatsu.core.util.ext.creationTime -import org.koitharu.kotatsu.core.util.ext.longHashCode -import org.koitharu.kotatsu.core.util.ext.toListSorted -import org.koitharu.kotatsu.core.util.ext.walkCompat -import org.koitharu.kotatsu.core.util.ext.withChildren -import org.koitharu.kotatsu.local.data.MangaIndex -import org.koitharu.kotatsu.local.data.hasCbzExtension -import org.koitharu.kotatsu.local.data.hasImageExtension -import org.koitharu.kotatsu.local.data.output.LocalMangaOutput -import org.koitharu.kotatsu.local.domain.model.LocalManga -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.parsers.util.toCamelCase -import java.io.File -import java.util.TreeMap -import java.util.zip.ZipFile - -/** - * Manga {Folder} - * |--- index.json (optional) - * |--- Chapter 1.cbz - * |--- Page 1.png - * : - * L--- Page x.png - * |--- Chapter 2.cbz - * : - * L--- Chapter x.cbz - */ -class LocalMangaDirInput(root: File) : LocalMangaInput(root) { - - override suspend fun getManga(): LocalManga = runInterruptible(Dispatchers.IO) { - val index = MangaIndex.read(File(root, LocalMangaOutput.ENTRY_NAME_INDEX)) - val mangaUri = root.toUri().toString() - val chapterFiles = getChaptersFiles() - val info = index?.getMangaInfo() - val cover = fileUri( - root, - index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(), - ) - val manga = info?.copy2( - source = LocalMangaSource, - url = mangaUri, - coverUrl = cover, - largeCoverUrl = cover, - chapters = info.chapters?.mapIndexedNotNull { i, c -> - val fileName = index.getChapterFileName(c.id) - val file = if (fileName != null) { - chapterFiles[fileName] - } else { - // old downloads - chapterFiles.values.elementAtOrNull(i) - } ?: return@mapIndexedNotNull null - c.copy(url = file.toUri().toString(), source = LocalMangaSource) - }, - ) ?: Manga( - id = root.absolutePath.longHashCode(), - title = root.name.toHumanReadable(), - url = mangaUri, - publicUrl = mangaUri, - source = LocalMangaSource, - coverUrl = findFirstImageEntry().orEmpty(), - chapters = chapterFiles.values.mapIndexed { i, f -> - MangaChapter( - id = "$i${f.name}".longHashCode(), - name = f.nameWithoutExtension.toHumanReadable(), - number = 0f, - volume = 0, - source = LocalMangaSource, - uploadDate = f.creationTime, - url = f.toUri().toString(), - scanlator = null, - branch = null, - ) - }, - altTitle = null, - rating = -1f, - isNsfw = false, - tags = setOf(), - state = null, - author = null, - largeCoverUrl = null, - description = null, - ) - LocalManga(manga, root) - } - - override suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) { - val index = MangaIndex.read(File(root, LocalMangaOutput.ENTRY_NAME_INDEX)) - index?.getMangaInfo() - } - - override suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { - val file = chapter.url.toUri().toFile() - if (file.isDirectory) { - file.withChildren { children -> - children - .filter { it.isFile && hasImageExtension(it) } - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) - }.map { - val pageUri = it.toUri().toString() - MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource) - } - } else { - ZipFile(file).use { zip -> - zip.entries() - .asSequence() - .filter { x -> !x.isDirectory } - .map { it.name } - .toListSorted(AlphanumComparator()) - .map { - val pageUri = zipUri(file, it) - MangaPage( - id = pageUri.longHashCode(), - url = pageUri, - preview = null, - source = LocalMangaSource, - ) - } - } - } - } - - private fun String.toHumanReadable() = replace("_", " ").toCamelCase() - - 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(includeDirectories = false) - .firstOrNull { hasImageExtension(it) }?.toUri()?.toString() - ?: run { - val cbz = root.walkCompat(includeDirectories = false) - .firstOrNull { it.hasCbzExtension() } ?: return null - ZipFile(cbz).use { zip -> - zip.entries().asSequence() - .firstOrNull { !it.isDirectory && hasImageExtension(it.name) } - ?.let { zipUri(cbz, it.name) } - } - } - } - - private fun fileUri(base: File, name: String): String { - return File(base, name).toUri().toString() - } - - private fun File.isChapterDirectory(): Boolean { - return isDirectory && withChildren { children -> children.any { hasImageExtension(it) } } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt deleted file mode 100644 index 7b01c469f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt +++ /dev/null @@ -1,111 +0,0 @@ -package org.koitharu.kotatsu.local.data.input - -import android.net.Uri -import androidx.core.net.toFile -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch -import org.koitharu.kotatsu.core.util.ext.toZipUri -import org.koitharu.kotatsu.local.data.hasCbzExtension -import org.koitharu.kotatsu.local.domain.model.LocalManga -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.parsers.util.toFileNameSafe -import java.io.File - -sealed class LocalMangaInput( - protected val root: File, -) { - - abstract suspend fun getManga(): LocalManga - - abstract suspend fun getMangaInfo(): Manga? - - abstract suspend fun getPages(chapter: MangaChapter): List - - companion object { - - fun of(manga: Manga): LocalMangaInput = of(Uri.parse(manga.url).toFile()) - - fun of(chapter: MangaChapter): LocalMangaInput = of(Uri.parse(chapter.url).toFile()) - - fun of(file: File): LocalMangaInput = when { - file.isDirectory -> LocalMangaDirInput(file) - else -> LocalMangaZipInput(file) - } - - fun ofOrNull(file: File): LocalMangaInput? = when { - file.isDirectory -> LocalMangaDirInput(file) - hasCbzExtension(file.name) -> LocalMangaZipInput(file) - else -> null - } - - suspend fun find(roots: Iterable, manga: Manga): LocalMangaInput? = channelFlow { - val fileName = manga.title.toFileNameSafe() - for (root in roots) { - launch { - val dir = File(root, fileName) - val zip = File(root, "$fileName.cbz") - val input = when { - dir.isDirectory -> LocalMangaDirInput(dir) - zip.isFile -> LocalMangaZipInput(zip) - else -> null - } - val info = runCatchingCancellable { input?.getMangaInfo() }.getOrNull() - if (info?.id == manga.id) { - send(input) - } - } - } - }.flowOn(Dispatchers.Default).firstOrNull() - - @JvmStatic - protected fun zipUri(file: File, entryName: String): String = file.toZipUri(entryName).toString() - - @JvmStatic - protected fun Manga.copy2( - url: String, - coverUrl: String, - largeCoverUrl: String, - chapters: List?, - source: MangaSource, - ) = Manga( - id = id, - title = title, - altTitle = altTitle, - url = url, - publicUrl = publicUrl, - rating = rating, - isNsfw = isNsfw, - coverUrl = coverUrl, - tags = tags, - state = state, - author = author, - largeCoverUrl = largeCoverUrl, - description = description, - chapters = chapters, - source = source, - ) - - @JvmStatic - protected fun MangaChapter.copy( - url: String, - source: MangaSource, - ) = MangaChapter( - id = id, - name = name, - number = number, - volume = volume, - url = url, - scanlator = scanlator, - uploadDate = uploadDate, - branch = branch, - source = source, - ) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaParser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaParser.kt new file mode 100644 index 000000000..02ffd9381 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaParser.kt @@ -0,0 +1,309 @@ +package org.koitharu.kotatsu.local.data.input + +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.core.net.toFile +import androidx.core.net.toUri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toOkioPath +import okio.Path.Companion.toPath +import okio.openZip +import org.jetbrains.annotations.Blocking +import org.koitharu.kotatsu.core.model.LocalMangaSource +import org.koitharu.kotatsu.core.util.AlphanumComparator +import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP +import org.koitharu.kotatsu.core.util.ext.isFileUri +import org.koitharu.kotatsu.core.util.ext.isRegularFile +import org.koitharu.kotatsu.core.util.ext.isZipUri +import org.koitharu.kotatsu.core.util.ext.longHashCode +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.toListSorted +import org.koitharu.kotatsu.local.data.MangaIndex +import org.koitharu.kotatsu.local.data.isZipArchive +import org.koitharu.kotatsu.local.data.output.LocalMangaOutput.Companion.ENTRY_NAME_INDEX +import org.koitharu.kotatsu.local.domain.model.LocalManga +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.toCamelCase +import org.koitharu.kotatsu.parsers.util.toFileNameSafe +import java.io.File + +/** + * Manga root {dir or zip file} + * |--- index.json (optional) + * |--- Page 1.png + * |--- Page 2.png + * |---Chapter 1/(dir or zip, optional) + * |------Page 1.1.png + * : + * L--- Page x.png + */ +class LocalMangaParser(private val uri: Uri) { + + constructor(file: File) : this(file.toUri()) + + private val rootFile: File = File(uri.schemeSpecificPart) + + suspend fun getManga(withDetails: Boolean): LocalManga = runInterruptible(Dispatchers.IO) { + val (fileSystem, rootPath) = uri.resolveFsAndPath() + val index = MangaIndex.read(fileSystem, rootPath / ENTRY_NAME_INDEX) + val mangaInfo = index?.getMangaInfo() + if (mangaInfo != null) { + val coverEntry: Path? = index.getCoverEntry()?.let { rootPath / it } ?: fileSystem.findFirstImage(rootPath) + mangaInfo.copyInternal( + source = LocalMangaSource, + url = rootFile.toUri().toString(), + coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() }.orEmpty(), + largeCoverUrl = null, + chapters = if (withDetails) { + mangaInfo.chapters?.map { c -> + c.copyInternal( + url = index.getChapterFileName(c.id)?.toPath()?.let { + uri.child(it, resolve = false).toString() + } ?: uri.toString(), + source = LocalMangaSource, + ) + } + } else { + null + }, + ) + } else { + val title = rootFile.nameWithoutExtension.replace("_", " ").toCamelCase() + val coverEntry = fileSystem.findFirstImage(rootPath) + val mimeTypeMap = MimeTypeMap.getSingleton() + Manga( + id = rootFile.absolutePath.longHashCode(), + title = title, + url = rootFile.toUri().toString(), + publicUrl = rootFile.toUri().toString(), + source = LocalMangaSource, + coverUrl = coverEntry?.let { + uri.child(it, resolve = true).toString() + }.orEmpty(), + chapters = if (withDetails) { + val chapters = fileSystem.listRecursively(rootPath) + .mapNotNullTo(HashSet()) { path -> + if (path != coverEntry && fileSystem.isRegularFile(path) && mimeTypeMap.isImage(path)) { + path.parent + } else { + null + } + }.sortedWith(compareBy(AlphanumComparator()) { x -> x.toString() }) + chapters.mapIndexed { i, p -> + val s = if (p.root == rootPath.root) { + p.relativeTo(rootPath).toString() + } else { + p + }.toString().removePrefix(Path.DIRECTORY_SEPARATOR) + MangaChapter( + id = "$i$s".longHashCode(), + name = s.ifEmpty { title }, + number = 0f, + volume = 0, + source = LocalMangaSource, + uploadDate = 0L, + url = uri.child(p.relativeTo(rootPath), resolve = false).toString(), + scanlator = null, + branch = null, + ) + } + } else { + null + }, + altTitle = null, + rating = -1f, + isNsfw = false, + tags = setOf(), + state = null, + author = null, + largeCoverUrl = null, + description = null, + ) + }.let { LocalManga(it, rootFile) } + } + + suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) { + val (fileSystem, rootPath) = uri.resolveFsAndPath() + val index = MangaIndex.read(fileSystem, rootPath / ENTRY_NAME_INDEX) + index?.getMangaInfo() + } + + suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { + val chapterUri = chapter.url.toUri().resolve() + val (fileSystem, rootPath) = chapterUri.resolveFsAndPath() + val index = MangaIndex.read(fileSystem, rootPath / ENTRY_NAME_INDEX) + val entries = fileSystem.listRecursively(rootPath) + .filter { fileSystem.isRegularFile(it) } + if (index != null) { + val pattern = index.getChapterNamesPattern(chapter) + entries.filter { x -> x.name.substringBefore('.').matches(pattern) } + } else { + val mimeTypeMap = MimeTypeMap.getSingleton() + entries.filter { x -> + mimeTypeMap.isImage(x) && x.parent == rootPath + } + }.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() }) + .map { x -> + val entryUri = chapterUri.child(x, resolve = true).toString() + MangaPage( + id = entryUri.longHashCode(), + url = entryUri, + preview = null, + source = LocalMangaSource, + ) + } + } + + private fun Uri.child(path: Path, resolve: Boolean): Uri { + val builder = buildUpon() + if (isZipUri() || !resolve) { + builder.fragment(path.toString().removePrefix(Path.DIRECTORY_SEPARATOR)) + } else { + val file = toFile() + 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() + } + + companion object { + + @Blocking + fun getOrNull(file: File): LocalMangaParser? = if ((file.isDirectory || file.isZipArchive) && file.canRead()) { + LocalMangaParser(file) + } else { + null + } + + suspend fun find(roots: Iterable, manga: Manga): LocalMangaParser? = channelFlow { + val fileName = manga.title.toFileNameSafe() + for (root in roots) { + launch { + val parser = getOrNull(File(root, fileName)) ?: getOrNull(File(root, "$fileName.cbz")) + val info = runCatchingCancellable { parser?.getMangaInfo() }.getOrNull() + if (info?.id == manga.id) { + send(parser) + } + } + } + }.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 { + val mimeTypeMap = MimeTypeMap.getSingleton() + if (recursive) { + listRecursively(rootPath) + } else { + list(rootPath).asSequence() + }.filter { isRegularFile(it) && mimeTypeMap.isImage(it) } + .toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() }) + .firstOrNull() + }.onFailure { e -> + e.printStackTraceDebug() + }.getOrNull() + + private fun MimeTypeMap.isImage(path: Path): Boolean = + getMimeTypeFromExtension(path.name.substringAfterLast('.')) + ?.startsWith("image/") == true + + private fun Uri.resolve(): Uri = if (isFileUri()) { + val file = toFile() + if (file.isZipArchive) { + this + } else if (file.isDirectory) { + file.resolve(fragment.orEmpty()).toUri() + } else { + this + } + } else { + this + } + + @Blocking + private fun Uri.resolveFsAndPath(): Pair { + val resolved = resolve() + return when { + resolved.isZipUri() -> { + FileSystem.SYSTEM.openZip(resolved.schemeSpecificPart.toPath()) to resolved.fragment.orEmpty() + .toRootedPath() + } + + isFileUri() -> { + val file = toFile() + if (file.isZipArchive) { + FileSystem.SYSTEM.openZip(schemeSpecificPart.toPath()) to fragment.orEmpty().toRootedPath() + } else { + FileSystem.SYSTEM to file.toOkioPath() + } + } + + else -> throw IllegalArgumentException("Unsupported uri $resolved") + } + } + + private fun String.toRootedPath(): Path = if (startsWith(Path.DIRECTORY_SEPARATOR)) { + this + } else { + Path.DIRECTORY_SEPARATOR + this + }.toPath() + + private fun Manga.copyInternal( + url: String = this.url, + coverUrl: String = this.coverUrl, + largeCoverUrl: String? = this.largeCoverUrl, + chapters: List? = this.chapters, + source: MangaSource = this.source, + ): Manga = Manga( + id = id, + title = title, + altTitle = altTitle, + url = url, + publicUrl = publicUrl, + rating = rating, + isNsfw = isNsfw, + coverUrl = coverUrl, + tags = tags, + state = state, + author = author, + largeCoverUrl = largeCoverUrl, + description = description, + chapters = chapters, + source = source, + ) + + private fun MangaChapter.copyInternal( + url: String = this.url, + source: MangaSource = this.source, + ) = MangaChapter( + id = id, + name = name, + number = number, + volume = volume, + url = url, + scanlator = scanlator, + uploadDate = uploadDate, + branch = branch, + source = source, + ) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt deleted file mode 100644 index ec33cd83c..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt +++ /dev/null @@ -1,155 +0,0 @@ -package org.koitharu.kotatsu.local.data.input - -import android.net.Uri -import android.webkit.MimeTypeMap -import androidx.collection.ArraySet -import androidx.core.net.toFile -import androidx.core.net.toUri -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible -import org.koitharu.kotatsu.core.model.LocalMangaSource -import org.koitharu.kotatsu.core.util.AlphanumComparator -import org.koitharu.kotatsu.core.util.ext.longHashCode -import org.koitharu.kotatsu.core.util.ext.readText -import org.koitharu.kotatsu.core.util.ext.toListSorted -import org.koitharu.kotatsu.local.data.MangaIndex -import org.koitharu.kotatsu.local.data.output.LocalMangaOutput -import org.koitharu.kotatsu.local.domain.model.LocalManga -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.parsers.util.toCamelCase -import java.io.File -import java.util.Enumeration -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - -/** - * Manga archive {.cbz or .zip file} - * |--- index.json (optional) - * |--- Page 1.png - * |--- Page 2.png - * : - * L--- Page x.png - */ -class LocalMangaZipInput(root: File) : LocalMangaInput(root) { - - override suspend fun getManga(): LocalManga { - val manga = runInterruptible(Dispatchers.IO) { - ZipFile(root).use { zip -> - val fileUri = root.toUri().toString() - val entry = zip.getEntry(LocalMangaOutput.ENTRY_NAME_INDEX) - val index = entry?.let(zip::readText)?.let(::MangaIndex) - val info = index?.getMangaInfo() - if (info != null) { - val cover = zipUri( - root, - entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(), - ) - return@use info.copy2( - source = LocalMangaSource, - url = fileUri, - coverUrl = cover, - largeCoverUrl = cover, - chapters = info.chapters?.map { c -> - c.copy(url = fileUri, source = LocalMangaSource) - }, - ) - } - // fallback - val title = root.nameWithoutExtension.replace("_", " ").toCamelCase() - val chapters = ArraySet() - for (x in zip.entries()) { - if (!x.isDirectory) { - chapters += x.name.substringBeforeLast(File.separatorChar, "") - } - } - val uriBuilder = root.toUri().buildUpon() - Manga( - id = root.absolutePath.longHashCode(), - title = title, - url = fileUri, - publicUrl = fileUri, - source = LocalMangaSource, - coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()), - chapters = chapters.sortedWith(AlphanumComparator()) - .mapIndexed { i, s -> - MangaChapter( - id = "$i$s".longHashCode(), - name = s.ifEmpty { title }, - number = 0f, - volume = 0, - source = LocalMangaSource, - uploadDate = 0L, - url = uriBuilder.fragment(s).build().toString(), - scanlator = null, - branch = null, - ) - }, - altTitle = null, - rating = -1f, - isNsfw = false, - tags = setOf(), - state = null, - author = null, - largeCoverUrl = null, - description = null, - ) - } - } - return LocalManga(manga, root) - } - - override suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) { - ZipFile(root).use { zip -> - val entry = zip.getEntry(LocalMangaOutput.ENTRY_NAME_INDEX) - val index = entry?.let(zip::readText)?.let(::MangaIndex) - index?.getMangaInfo() - } - } - - override suspend fun getPages(chapter: MangaChapter): List { - return runInterruptible(Dispatchers.IO) { - val uri = Uri.parse(chapter.url) - val file = uri.toFile() - ZipFile(file).use { zip -> - val index = zip.getEntry(LocalMangaOutput.ENTRY_NAME_INDEX)?.let(zip::readText)?.let(::MangaIndex) - var entries = zip.entries().asSequence() - entries = if (index != null) { - val pattern = index.getChapterNamesPattern(chapter) - entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) } - } else { - val parent = uri.fragment.orEmpty() - entries.filter { x -> - !x.isDirectory && x.name.substringBeforeLast( - File.separatorChar, - "", - ) == parent - } - } - entries - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) - .map { x -> - val entryUri = zipUri(file, x.name) - MangaPage( - id = entryUri.longHashCode(), - url = entryUri, - preview = null, - source = LocalMangaSource, - ) - } - } - } - } - - private fun findFirstImageEntry(entries: Enumeration): ZipEntry? { - val list = entries.toList() - .filterNot { it.isDirectory } - .sortedWith(compareBy(AlphanumComparator()) { x -> x.name }) - val map = MimeTypeMap.getSingleton() - return list.firstOrNull { - map.getMimeTypeFromExtension(it.name.substringAfterLast('.')) - ?.startsWith("image/") == true - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt index 680df9432..4c1cbff62 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt @@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.local.data.MangaIndex -import org.koitharu.kotatsu.local.data.input.LocalMangaDirInput +import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.toFileNameSafe @@ -96,7 +96,7 @@ class LocalMangaDirOutput( } suspend fun deleteChapters(ids: Set) = mutex.withLock { - val chapters = checkNotNull((index.getMangaInfo() ?: LocalMangaDirInput(rootFile).getManga().manga).chapters) { + val chapters = checkNotNull((index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters) { "No chapters found" }.withIndex() val victimsIds = ids.toMutableSet() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt index c150ac6a4..6db94d18d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.withContext import okio.Closeable import org.koitharu.kotatsu.core.prefs.DownloadFormat import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug -import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -100,7 +100,7 @@ sealed class LocalMangaOutput( private suspend fun canWriteTo(file: File, manga: Manga): Boolean { val info = runCatchingCancellable { - LocalMangaInput.of(file).getMangaInfo() + LocalMangaParser(file).getMangaInfo() }.onFailure { it.printStackTraceDebug() }.getOrNull() ?: return false diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt index 38fe8ed21..2b535e28b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt @@ -29,7 +29,7 @@ abstract class LocalObserveMapper( val mapped = if (m.isLocal) { m } else { - localMangaIndex.get(m.id)?.manga + localMangaIndex.get(m.id, withDetails = false)?.manga } mapped?.let { mm -> toResult(item, mm) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt index 2d8e8941e..ed0749933 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.local.domain.model +import android.net.Uri import androidx.core.net.toFile import androidx.core.net.toUri import org.koitharu.kotatsu.core.util.ext.creationTime @@ -21,6 +22,8 @@ data class LocalManga( return field } + fun toUri(): Uri = manga.url.toUri() + fun isMatchesQuery(query: String): Boolean { return manga.title.contains(query, ignoreCase = true) || manga.altTitle?.contains(query, ignoreCase = true) == true || diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 3b367071b..9de3dffdb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -1,10 +1,8 @@ package org.koitharu.kotatsu.reader.domain -import android.content.ContentResolver.MimeTypeInfo import android.content.Context import android.graphics.Rect import android.net.Uri -import android.webkit.MimeTypeMap import androidx.annotation.AnyThread import androidx.collection.LongSparseArray import androidx.collection.set @@ -61,8 +59,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.core.image.BitmapDecoderCompat -import org.koitharu.kotatsu.core.util.ext.mimeType import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import java.io.File import java.util.LinkedList