From a3ef1766a1e730bffe2424dff8abb3de01276dce Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 9 Mar 2022 18:21:13 +0200 Subject: [PATCH] Add ComicKFun manga parser --- .../kotatsu/core/model/MangaSource.kt | 1 + .../kotatsu/core/parser/ParserModule.kt | 1 + .../core/parser/site/ComickFunRepository.kt | 212 ++++++++++++++++++ .../kotatsu/utils/ext/CollectionExt.kt | 1 + .../org/koitharu/kotatsu/utils/ext/JsonExt.kt | 4 +- 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/site/ComickFunRepository.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 5ec3ff85a..af4e67704 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -36,5 +36,6 @@ enum class MangaSource( MANGAOWL("MangaOwl", "en"), MANGADEX("MangaDex", null), BATOTO("Bato.To", null), + COMICK_FUN("ComicK", null), ; } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index c1e145278..0431c2fdb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -33,4 +33,5 @@ val parserModule factory(named(MangaSource.MANGAOWL)) { MangaOwlRepository(get()) } factory(named(MangaSource.MANGADEX)) { MangaDexRepository(get()) } factory(named(MangaSource.BATOTO)) { BatoToRepository(get()) } + factory(named(MangaSource.COMICK_FUN)) { ComickFunRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ComickFunRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ComickFunRepository.kt new file mode 100644 index 000000000..bd0b858f1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ComickFunRepository.kt @@ -0,0 +1,212 @@ +package org.koitharu.kotatsu.core.parser.site + +import android.util.SparseArray +import androidx.collection.ArraySet +import org.json.JSONArray +import org.json.JSONObject +import org.koitharu.kotatsu.base.domain.MangaLoaderContext +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.utils.ext.* +import java.text.SimpleDateFormat +import java.util.* + +/** + * https://api.comick.fun/docs/static/index.html + */ + +private const val PAGE_SIZE = 20 +private const val CHAPTERS_LIMIT = 99999 + +class ComickFunRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { + + override val defaultDomain = "comick.fun" + override val source = MangaSource.COMICK_FUN + override val sortOrders: Set = EnumSet.of( + SortOrder.POPULARITY, + SortOrder.UPDATED, + SortOrder.RATING, + ) + + @Volatile + private var cachedTags: SparseArray? = null + + override suspend fun getList2( + offset: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder? + ): List { + val domain = getDomain() + val url = buildString { + append("https://api.") + append(domain) + append("/search?tachiyomi=true") + if (!query.isNullOrEmpty()) { + if (offset > 0) { + return emptyList() + } + append("&q=") + append(query.urlEncoded()) + } else { + append("&limit=") + append(PAGE_SIZE) + append("&page=") + append((offset / PAGE_SIZE) + 1) + if (!tags.isNullOrEmpty()) { + append("&genres=") + appendAll(tags, "&genres=", MangaTag::key) + } + append("&sort=") // view, uploaded, rating, follow, user_follow_count + append( + when (sortOrder) { + SortOrder.POPULARITY -> "view" + SortOrder.RATING -> "rating" + else -> "uploaded" + } + ) + } + } + val ja = loaderContext.httpGet(url).parseJsonArray() + val tagsMap = cachedTags ?: loadTags() + return ja.map { jo -> + val slug = jo.getString("slug") + Manga( + id = generateUid(slug), + title = jo.getString("title"), + altTitle = null, + url = slug, + publicUrl = "https://$domain/comic/$slug", + rating = jo.getDouble("rating").toFloat() / 10f, + isNsfw = false, + coverUrl = jo.getString("cover_url"), + largeCoverUrl = null, + description = jo.getStringOrNull("desc"), + tags = jo.selectGenres("genres", tagsMap), + state = runCatching { + if (jo.getBoolean("translation_completed")) { + MangaState.FINISHED + } else { + MangaState.ONGOING + } + }.getOrNull(), + author = null, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val domain = getDomain() + val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true" + val jo = loaderContext.httpGet(url).parseJson() + val comic = jo.getJSONObject("comic") + return manga.copy( + title = comic.getString("title"), + altTitle = null, // TODO + isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"), + description = comic.getStringOrNull("parsed") ?: comic.getString("desc"), + tags = manga.tags + jo.getJSONArray("genres").mapToSet { + MangaTag( + title = it.getString("name"), + key = it.getString("slug"), + source = source, + ) + }, + author = jo.getJSONArray("artists").optJSONObject(0)?.getString("name"), + chapters = getChapters(comic.getLong("id")), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val jo = loaderContext.httpGet( + "https://api.${getDomain()}/chapter/${chapter.url}?tachiyomi=true" + ).parseJson().getJSONObject("chapter") + val referer = "https://${getDomain()}/" + return jo.getJSONArray("images").map { + val url = it.getString("url") + MangaPage( + id = generateUid(url), + url = url, + referer = referer, + preview = null, + source = source, + ) + } + } + + override suspend fun getTags(): Set { + val sparseArray = cachedTags ?: loadTags() + val set = ArraySet(sparseArray.size()) + for (i in 0 until sparseArray.size()) { + set.add(sparseArray.valueAt(i)) + } + return set + } + + private suspend fun loadTags(): SparseArray { + val ja = loaderContext.httpGet("https://api.${getDomain()}/genre").parseJsonArray() + val tags = SparseArray(ja.length()) + for (jo in ja) { + tags.append( + jo.getInt("id"), + MangaTag( + title = jo.getString("name"), + key = jo.getString("slug"), + source = source, + ) + ) + } + cachedTags = tags + return tags + } + + private suspend fun getChapters(id: Long): List { + val ja = loaderContext.httpGet( + url = "https://api.${getDomain()}/comic/$id/chapter?tachiyomi=true&limit=$CHAPTERS_LIMIT" + ).parseJson().getJSONArray("chapters") + val dateFormat = SimpleDateFormat("yyyy-MM-dd") + val counters = HashMap() + return ja.mapReversed { jo -> + val locale = Locale.forLanguageTag(jo.getString("lang")) + var number = counters[locale] ?: 0 + number++ + counters[locale] = number + MangaChapter( + id = generateUid(jo.getLong("id")), + name = buildString { + jo.getStringOrNull("vol")?.let { append("Vol ").append(it).append(' ') } + jo.getStringOrNull("chap")?.let { append("Chap ").append(it) } + jo.getStringOrNull("title")?.let { append(": ").append(it) } + }, + number = number, + url = jo.getString("hid"), + scanlator = jo.optJSONArray("group_name")?.optString(0), + uploadDate = dateFormat.tryParse(jo.getString("created_at").substringBefore('T')), + branch = locale.getDisplayName(locale).toTitleCase(locale), + source = source, + ) + } + } + + private inline fun JSONArray.mapReversed(block: (JSONObject) -> R): List { + val len = length() + val destination = ArrayList(len) + for (i in (0 until len).reversed()) { + val jo = getJSONObject(i) + destination.add(block(jo)) + } + return destination + } + + private fun JSONObject.selectGenres(name: String, tags: SparseArray): Set { + val array = optJSONArray(name) ?: return emptySet() + val res = ArraySet(array.length()) + for (i in 0 until array.length()) { + val id = array.getInt(i) + val tag = tags.get(id) ?: continue + res.add(tag) + } + return res + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt index 3aa96dc2a..441b87c31 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.utils.ext +import android.util.SparseArray import androidx.collection.ArrayMap import androidx.collection.ArraySet import androidx.collection.LongSparseArray diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt index 0efa24264..f5c55b49d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt @@ -52,7 +52,9 @@ fun JSONArray.mapIndexed(block: (Int, JSONObject) -> T): List { fun JSONObject.getStringOrNull(name: String): String? = opt(name)?.takeUnless { it === JSONObject.NULL -}?.toString() +}?.toString()?.takeUnless { + it.isEmpty() +} fun JSONObject.getBooleanOrDefault(name: String, defaultValue: Boolean): Boolean = opt(name)?.takeUnless { it === JSONObject.NULL