From b3933848e983042a0613bf12d425b55a8a9bb1b6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 22 Jan 2025 10:15:12 +0200 Subject: [PATCH] Local manga parsing fixes --- .../koitharu/kotatsu/local/data/MangaIndex.kt | 169 +++++++++++------- .../local/data/input/LocalMangaParser.kt | 6 +- gradle/libs.versions.toml | 2 +- 3 files changed, 105 insertions(+), 72 deletions(-) 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 ae7daef04..b3f95bd00 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 @@ -12,13 +12,15 @@ 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.ContentRating import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.util.find +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault +import org.koitharu.kotatsu.parsers.util.json.getEnumValueOrNull import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault @@ -34,120 +36,120 @@ class MangaIndex(source: String?) { fun setMangaInfo(manga: Manga) { require(!manga.isLocal) { "Local manga information cannot be stored" } - json.put("id", manga.id) - json.put("title", manga.title) - json.put("title_alt", manga.altTitle) - json.put("url", manga.url) - json.put("public_url", manga.publicUrl) - json.put("author", manga.author) - json.put("cover", manga.coverUrl) - json.put("description", manga.description) - json.put("rating", manga.rating) - json.put("nsfw", manga.isNsfw) - json.put("state", manga.state?.name) - json.put("source", manga.source.name) - json.put("cover_large", manga.largeCoverUrl) + json.put(KEY_ID, manga.id) + json.put(KEY_TITLE, manga.title) + json.put(KEY_TITLE_ALT, manga.altTitle) + json.put(KEY_URL, manga.url) + json.put(KEY_PUBLIC_URL, manga.publicUrl) + json.put(KEY_AUTHOR, manga.author) + json.put(KEY_COVER, manga.coverUrl) + json.put(KEY_DESCRIPTION, manga.description) + json.put(KEY_RATING, manga.rating) + json.put(KEY_CONTENT_RATING, manga.contentRating) + json.put(KEY_NSFW, manga.isNsfw) // for backward compatibility + json.put(KEY_STATE, manga.state?.name) + json.put(KEY_SOURCE, manga.source.name) + json.put(KEY_COVER_LARGE, manga.largeCoverUrl) json.put( - "tags", + KEY_TAGS, JSONArray().also { a -> for (tag in manga.tags) { val jo = JSONObject() - jo.put("key", tag.key) - jo.put("title", tag.title) + jo.put(KEY_KEY, tag.key) + jo.put(KEY_TITLE, tag.title) a.put(jo) } }, ) - if (!json.has("chapters")) { - json.put("chapters", JSONObject()) + if (!json.has(KEY_CHAPTERS)) { + json.put(KEY_CHAPTERS, JSONObject()) } - json.put("app_id", BuildConfig.APPLICATION_ID) - json.put("app_version", BuildConfig.VERSION_CODE) + json.put(KEY_APP_ID, BuildConfig.APPLICATION_ID) + json.put(KEY_APP_VERSION, BuildConfig.VERSION_CODE) } fun getMangaInfo(): Manga? = if (json.length() == 0) null else runCatching { - val source = MangaSource(json.getString("source")) + val source = MangaSource(json.getString(KEY_SOURCE)) Manga( - id = json.getLong("id"), - title = json.getString("title"), - altTitle = json.getStringOrNull("title_alt"), - url = json.getString("url"), - publicUrl = json.getStringOrNull("public_url").orEmpty(), - author = json.getStringOrNull("author"), - largeCoverUrl = json.getStringOrNull("cover_large"), + id = json.getLong(KEY_ID), + title = json.getString(KEY_TITLE), + altTitle = json.getStringOrNull(KEY_TITLE_ALT), + url = json.getString(KEY_URL), + publicUrl = json.getStringOrNull(KEY_PUBLIC_URL).orEmpty(), + author = json.getStringOrNull(KEY_AUTHOR), + largeCoverUrl = json.getStringOrNull(KEY_COVER_LARGE), source = source, - rating = json.getDouble("rating").toFloat(), - isNsfw = json.getBooleanOrDefault("nsfw", false), - coverUrl = json.getString("cover"), - state = json.getStringOrNull("state")?.let { stateString -> - MangaState.entries.find(stateString) - }, - description = json.getStringOrNull("description"), - tags = json.getJSONArray("tags").mapJSONToSet { x -> + rating = json.getFloatOrDefault(KEY_RATING, RATING_UNKNOWN), + contentRating = json.getEnumValueOrNull(KEY_CONTENT_RATING, ContentRating::class.java) + ?: if (json.getBooleanOrDefault(KEY_NSFW, false)) ContentRating.ADULT else null, + coverUrl = json.getStringOrNull(KEY_COVER), + state = json.getEnumValueOrNull(KEY_STATE, MangaState::class.java), + description = json.getStringOrNull(KEY_DESCRIPTION), + tags = json.getJSONArray(KEY_TAGS).mapJSONToSet { x -> MangaTag( - title = x.getString("title").toTitleCase(), - key = x.getString("key"), + title = x.getString(KEY_TITLE).toTitleCase(), + key = x.getString(KEY_KEY), source = source, ) }, - chapters = getChapters(json.getJSONObject("chapters"), source), + chapters = getChapters(json.getJSONObject(KEY_CHAPTERS), source), ) }.getOrNull() - fun getCoverEntry(): String? = json.getStringOrNull("cover_entry") + fun getCoverEntry(): String? = json.getStringOrNull(KEY_COVER_ENTRY) fun addChapter(chapter: IndexedValue, filename: String?) { - val chapters = json.getJSONObject("chapters") + val chapters = json.getJSONObject(KEY_CHAPTERS) if (!chapters.has(chapter.value.id.toString())) { val jo = JSONObject() - jo.put("number", chapter.value.number) - jo.put("volume", chapter.value.volume) - jo.put("url", chapter.value.url) - jo.put("name", chapter.value.name) - jo.put("uploadDate", chapter.value.uploadDate) - jo.put("scanlator", chapter.value.scanlator) - jo.put("branch", chapter.value.branch) - jo.put("entries", "%08d_%03d\\d{3}".format(chapter.value.branch.hashCode(), chapter.index + 1)) - jo.put("file", filename) + jo.put(KEY_NUMBER, chapter.value.number) + jo.put(KEY_VOLUME, chapter.value.volume) + jo.put(KEY_URL, chapter.value.url) + jo.put(KEY_NAME, chapter.value.name) + jo.put(KEY_UPLOAD_DATE, chapter.value.uploadDate) + jo.put(KEY_SCANLATOR, chapter.value.scanlator) + jo.put(KEY_BRANCH, chapter.value.branch) + jo.put(KEY_ENTRIES, "%08d_%03d\\d{3}".format(chapter.value.branch.hashCode(), chapter.index + 1)) + jo.put(KEY_FILE, filename) chapters.put(chapter.value.id.toString(), jo) } } fun removeChapter(id: Long): Boolean { - return json.has("chapters") && json.getJSONObject("chapters").remove(id.toString()) != null + return json.has(KEY_CHAPTERS) && json.getJSONObject(KEY_CHAPTERS).remove(id.toString()) != null } fun getChapterFileName(chapterId: Long): String? { - return json.optJSONObject("chapters")?.optJSONObject(chapterId.toString())?.getStringOrNull("file") + return json.optJSONObject(KEY_CHAPTERS)?.optJSONObject(chapterId.toString())?.getStringOrNull(KEY_FILE) } fun setCoverEntry(name: String) { - json.put("cover_entry", name) + json.put(KEY_COVER_ENTRY, name) } fun getChapterNamesPattern(chapter: MangaChapter) = Regex( - json.getJSONObject("chapters") + json.getJSONObject(KEY_CHAPTERS) .getJSONObject(chapter.id.toString()) - .getString("entries"), + .getString(KEY_ENTRIES), ) fun sortChaptersByName() { - val jo = json.getJSONObject("chapters") + val jo = json.getJSONObject(KEY_CHAPTERS) val list = ArrayList(jo.length()) jo.keys().forEach { id -> val item = jo.getJSONObject(id) - item.put("id", id) + item.put(KEY_ID, id) list.add(item) } val comparator = org.koitharu.kotatsu.core.util.AlphanumComparator() - list.sortWith(compareBy(comparator) { it.getString("name") }) + list.sortWith(compareBy(comparator) { it.getString(KEY_NAME) }) val newJo = JSONObject() list.forEachIndexed { i, obj -> - obj.put("number", i + 1) - val id = obj.remove("id") as String + obj.put(KEY_NUMBER, i + 1) + val id = obj.remove(KEY_ID) as String newJo.put(id, obj) } - json.put("chapters", newJo) + json.put(KEY_CHAPTERS, newJo) } fun clear() { @@ -171,13 +173,13 @@ class MangaIndex(source: String?) { chapters.add( MangaChapter( id = k.toLong(), - name = v.getString("name"), - url = v.getString("url"), - number = v.getFloatOrDefault("number", 0f), - volume = v.getIntOrDefault("volume", 0), - uploadDate = v.getLongOrDefault("uploadDate", 0L), - scanlator = v.getStringOrNull("scanlator"), - branch = v.getStringOrNull("branch"), + name = v.getString(KEY_NAME), + url = v.getString(KEY_URL), + number = v.getFloatOrDefault(KEY_NUMBER, 0f), + volume = v.getIntOrDefault(KEY_VOLUME, 0), + uploadDate = v.getLongOrDefault(KEY_UPLOAD_DATE, 0L), + scanlator = v.getStringOrNull(KEY_SCANLATOR), + branch = v.getStringOrNull(KEY_BRANCH), source = source, ), ) @@ -193,6 +195,35 @@ class MangaIndex(source: String?) { companion object { + private const val KEY_ID = "id" + private const val KEY_TITLE = "title" + private const val KEY_TITLE_ALT = "title_alt" + private const val KEY_URL = "url" + private const val KEY_PUBLIC_URL = "public_url" + private const val KEY_AUTHOR = "author" + private const val KEY_COVER = "cover" + private const val KEY_DESCRIPTION = "description" + private const val KEY_RATING = "rating" + private const val KEY_CONTENT_RATING = "content_rating" + private const val KEY_NSFW = "nsfw" + private const val KEY_STATE = "state" + private const val KEY_SOURCE = "source" + private const val KEY_COVER_LARGE = "cover_large" + private const val KEY_TAGS = "tags" + private const val KEY_CHAPTERS = "chapters" + private const val KEY_NUMBER = "number" + private const val KEY_VOLUME = "volume" + private const val KEY_NAME = "name" + private const val KEY_UPLOAD_DATE = "uploadDate" + private const val KEY_SCANLATOR = "scanlator" + private const val KEY_BRANCH = "branch" + private const val KEY_ENTRIES = "entries" + private const val KEY_FILE = "file" + private const val KEY_COVER_ENTRY = "cover_entry" + private const val KEY_KEY = "key" + private const val KEY_APP_ID = "app_id" + private const val KEY_APP_VERSION = "app_version" + @Blocking @WorkerThread fun read(fileSystem: FileSystem, path: Path): MangaIndex? = runCatchingCancellable { 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 index abc9dfc55..89b3d66f0 100644 --- 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 @@ -128,7 +128,7 @@ class LocalMangaParser(private val uri: Uri) { }, altTitle = null, rating = -1f, - isNsfw = false, + contentRating = null, tags = setOf(), state = null, author = null, @@ -171,7 +171,7 @@ class LocalMangaParser(private val uri: Uri) { } private fun Uri.child(path: Path, resolve: Boolean): Uri { - val file = toFile() + val file = fileFromPath() val builder = buildUpon() val isZip = isZipUri() || file.isZipArchive if (isZip) { @@ -278,6 +278,8 @@ class LocalMangaParser(private val uri: Uri) { this } + private fun Uri.fileFromPath(): File = File(requireNotNull(path) { "Uri path is null: $this" }) + @Blocking private fun Uri.resolveFsAndPath(): FsAndPath { val resolved = resolve() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 498bcab50..bc8df7562 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ material = "1.13.0-alpha09" moshi = "1.15.2" okhttp = "4.12.0" okio = "3.10.2" -parsers = "b0a1cc48a6" +parsers = "51ed1b2db8" preference = "1.2.1" recyclerview = "1.4.0" room = "2.6.1"