From 16c4320b7db34399d376911b13aa51019835a6a9 Mon Sep 17 00:00:00 2001
From: ShadesOfRay <r.y.wang00@gmail.com>
Date: Sat, 1 May 2021 17:58:15 -0400
Subject: [PATCH] Fixed Kuaikanmanhua extension (#6783)

* Kkmh (#1)

* Fixed fetching popular and latest manga for KKMH

* Changed Searching and filtering to use the API for KKMH

* Chapter lists are now properly called and now show upload date

* Pages are properly called. Code cleanup. Extension properly works

Co-authored-by: Raymond Wang <rywang@email.wm.edu>

* Converted to HttpSource. More code cleanup.

Co-authored-by: Raymond Wang <rywang@email.wm.edu>
---
 src/zh/kuaikanmanhua/build.gradle             |   2 +-
 .../zh/kuaikanmanhua/Kuaikanmanhua.kt         | 192 ++++++++----------
 2 files changed, 91 insertions(+), 103 deletions(-)

diff --git a/src/zh/kuaikanmanhua/build.gradle b/src/zh/kuaikanmanhua/build.gradle
index b07633d9d..67d62985d 100644
--- a/src/zh/kuaikanmanhua/build.gradle
+++ b/src/zh/kuaikanmanhua/build.gradle
@@ -5,7 +5,7 @@ ext {
     extName = 'Kuaikanmanhua'
     pkgNameSuffix = 'zh.kuaikanmanhua'
     extClass = '.Kuaikanmanhua'
-    extVersionCode = 2
+    extVersionCode = 3
     libVersion = '1.2'
 }
 
diff --git a/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt b/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt
index 6b7bb4d1a..3fa8899bf 100644
--- a/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt
+++ b/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt
@@ -1,9 +1,5 @@
 package eu.kanade.tachiyomi.extension.zh.kuaikanmanhua
 
-import com.github.salomonbrys.kotson.fromJson
-import com.google.gson.Gson
-import com.google.gson.JsonArray
-import com.google.gson.JsonObject
 import eu.kanade.tachiyomi.network.GET
 import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.source.model.FilterList
@@ -11,15 +7,15 @@ import eu.kanade.tachiyomi.source.model.MangasPage
 import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.source.model.SChapter
 import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.ParsedHttpSource
-import eu.kanade.tachiyomi.util.asJsoup
+import eu.kanade.tachiyomi.source.online.HttpSource
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.Response
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
+import org.json.JSONArray
+import org.json.JSONObject
+import rx.Observable
 
-class Kuaikanmanhua : ParsedHttpSource() {
+class Kuaikanmanhua : HttpSource() {
 
     override val name = "Kuaikanmanhua"
 
@@ -31,65 +27,50 @@ class Kuaikanmanhua : ParsedHttpSource() {
 
     override val client: OkHttpClient = network.cloudflareClient
 
-    private val gson = Gson()
+    private val apiUrl = "https://api.kkmh.com"
 
     // Popular
 
     override fun popularMangaRequest(page: Int): Request {
-        return GET("$baseUrl/tag/0?state=1&page=$page", headers)
+        return GET("$apiUrl/v1/topic_new/lists/get_by_tag?tag=0&since=${(page - 1) * 10}", headers)
     }
 
     override fun popularMangaParse(response: Response): MangasPage {
-        return parseMangaDocument(response.asJsoup())
+        val body = response.body!!.string()
+        val jsonList = JSONObject(body).getJSONObject("data").getJSONArray("topics")
+        return parseMangaJsonArray(jsonList)
     }
 
-    private fun parseMangaDocument(document: Document): MangasPage {
-        val mangas = mutableListOf<SManga>()
+    private fun parseMangaJsonArray(jsonList: JSONArray, isSearch: Boolean = false): MangasPage {
+        val mangaList = mutableListOf<SManga>()
 
-        gson.fromJson<JsonArray>(
-            document.select("script:containsData(datalist)").first().data()
-                .substringAfter("dataList:").substringBefore("}],error")
-        )
-            .forEach { mangas.add(mangaFromJson(it.asJsonObject)) }
-
-        return MangasPage(mangas, document.select(popularMangaNextPageSelector()).isNotEmpty())
+        for (i in 0 until jsonList.length()) {
+            val obj = jsonList.getJSONObject(i)
+            mangaList.add(
+                SManga.create().apply {
+                    title = obj.getString("title")
+                    thumbnail_url = obj.getString("vertical_image_url")
+                    url = "/web/topic/" + obj.getInt("id")
+                }
+            )
+        }
+        // KKMH does not have pages when you search
+        return MangasPage(mangaList, mangaList.size > 9 && !isSearch)
     }
 
-    private fun mangaFromJson(jsonObject: JsonObject): SManga {
-        val manga = SManga.create()
-
-        manga.url = "/web/topic/" + jsonObject["id"].asString
-        manga.title = jsonObject["title"].asString
-        manga.thumbnail_url = jsonObject["cover_image_url"].asString
-
-        return manga
-    }
-
-    override fun popularMangaSelector() = throw UnsupportedOperationException("Not used")
-
-    override fun popularMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used")
-
-    override fun popularMangaNextPageSelector() = "li:not(.disabled) b.right"
-
     // Latest
 
     override fun latestUpdatesRequest(page: Int): Request {
-        return GET("$baseUrl/tag/19?state=1&page=$page", headers)
+        return GET("$apiUrl/v1/topic_new/lists/get_by_tag?tag=19&since=${(page - 1) * 10}", headers)
     }
 
     override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
 
-    override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used")
-
-    override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Not used")
-
-    override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used")
-
     // Search
 
     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         return if (query.isNotEmpty()) {
-            GET("$baseUrl/s/result/$query", headers)
+            GET("$apiUrl/v1/search/topic?q=$query&size=18", headers)
         } else {
             lateinit var genre: String
             lateinit var status: String
@@ -103,94 +84,97 @@ class Kuaikanmanhua : ParsedHttpSource() {
                     }
                 }
             }
-            GET("$baseUrl/tag/$genre?state=$status&page=$page", headers)
+            GET("$apiUrl/v1/search/by_tag?since=${(page - 1) * 10}&tag=$genre&sort=1&query_category=%7B%22update_status%22:$status%7D")
         }
     }
 
     override fun searchMangaParse(response: Response): MangasPage {
-        val mangas = mutableListOf<SManga>()
-        val document = response.asJsoup()
-
-        document.select("script:containsData(resultList)").let {
-            return if (it.isNotEmpty()) {
-                // for search by query
-                gson.fromJson<JsonArray>(
-                    it.first().data()
-                        .substringAfter("resultList:").substringBefore(",noResult")
-                )
-                    .forEach { result -> mangas.add(searchMangaFromJson(result.asJsonObject)) }
-                MangasPage(mangas, document.select(searchMangaNextPageSelector()).isNotEmpty())
-            } else {
-                // for search by genre, status
-                parseMangaDocument(document)
-            }
+        val body = response.body!!.string()
+        val jsonObj = JSONObject(body).getJSONObject("data")
+        if (jsonObj.has("hit")) {
+            return parseMangaJsonArray(jsonObj.getJSONArray("hit"), true)
         }
+
+        return parseMangaJsonArray(jsonObj.getJSONArray("topics"), false)
     }
 
-    private fun searchMangaFromJson(jsonObject: JsonObject): SManga {
-        val manga = SManga.create()
-
-        manga.url = jsonObject["url"].asString
-        manga.title = jsonObject["title"].asString
-        manga.thumbnail_url = jsonObject["image_url"].asString
-
-        return manga
-    }
-
-    override fun searchMangaSelector() = throw UnsupportedOperationException("Not used")
-
-    override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
-
-    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
-
     // Details
 
-    override fun mangaDetailsParse(document: Document): SManga {
-        val infoElement = document.select("div.TopicHeader").first()
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+        // Convert the stored url to one that works with the api
+        val newUrl = apiUrl + "/v1/topics/" + manga.url.trimEnd('/').substringAfterLast("/")
+        val response = client.newCall(GET(newUrl)).execute()
+        val sManga = mangaDetailsParse(response).apply { initialized = true }
+        return Observable.just(sManga)
+    }
 
+    override fun mangaDetailsParse(response: Response): SManga {
+        val data = JSONObject(response.body!!.string()).getJSONObject("data")
         val manga = SManga.create()
-        manga.title = infoElement.select("h3").first().text()
-        manga.author = infoElement.select("div.nickname").text()
-        manga.description = infoElement.select("div.detailsBox p").text()
+        manga.title = data.getString("title")
+        manga.author = data.getJSONObject("user").getString("nickname")
+        manga.description = data.getString("description")
+        manga.status = data.getInt("update_status_code")
 
         return manga
     }
 
     // Chapters & Pages
 
-    override fun chapterListSelector() = "div.TopicItem"
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
+        val newUrl = apiUrl + "/v1/topics/" + manga.url.trimEnd('/').substringAfterLast("/")
+        val response = client.newCall(GET(newUrl)).execute()
+        val chapters = chapterListParse(response)
+        return Observable.just(chapters)
+    }
 
-    override fun chapterFromElement(element: Element): SChapter {
-        val chapter = SChapter.create()
+    override fun chapterListParse(response: Response): List<SChapter> {
+        val data = JSONObject(response.body!!.string()).getJSONObject("data")
+        val chaptersJson = data.getJSONArray("comics")
+        val chapters = mutableListOf<SChapter>()
 
-        element.select("div.title a").let {
-            chapter.url = it.attr("href")
-            chapter.name = it.text() + if (element.select("i.lockedIcon").isNotEmpty()) { " \uD83D\uDD12" } else { "" }
+        for (i in 0 until chaptersJson.length()) {
+            val obj = chaptersJson.getJSONObject(i)
+            chapters.add(
+                SChapter.create().apply {
+                    url = "/v2/comic/" + obj.getString("id")
+                    name = obj.getString("title") +
+                        if (!obj.getBoolean("can_view")) {
+                            " \uD83D\uDD12"
+                        } else {
+                            ""
+                        }
+                    date_upload = obj.getLong("created_at") * 1000
+                }
+            )
         }
-        return chapter
+
+        return chapters
+    }
+
+    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
+        val request = client.newCall(pageListRequest(chapter)).execute()
+        return Observable.just(pageListParse(request))
     }
 
     override fun pageListRequest(chapter: SChapter): Request {
-        if (chapter.url == "javascript:void(0);") {
+        if (chapter.name.endsWith("🔒")) {
             throw Exception("[此章节为付费内容]")
         }
-        return super.pageListRequest(chapter)
+        return GET(apiUrl + chapter.url)
     }
 
-    override fun pageListParse(document: Document): List<Page> {
-        val pages = mutableListOf<Page>()
-
-        gson.fromJson<JsonArray>(
-            document.select("script:containsData(comicImages)").first().data()
-                .substringAfter("comicImages:").substringBefore("},nextComicInfo")
-        )
-            .forEachIndexed { i, json -> pages.add(Page(i, "", json.asJsonObject["url"].asString)) }
+    override fun pageListParse(response: Response): List<Page> {
+        val pages = ArrayList<Page>()
+        val data = JSONObject(response.body!!.string()).getJSONObject("data")
+        val pagesJson = data.getJSONArray("images")
 
+        for (i in 0 until pagesJson.length()) {
+            pages.add(Page(i, pagesJson.getString(i), pagesJson.getString(i)))
+        }
         return pages
     }
 
-    override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
-
     // Filters
 
     override fun getFilterList() = FilterList(
@@ -199,6 +183,10 @@ class Kuaikanmanhua : ParsedHttpSource() {
         GenreFilter()
     )
 
+    override fun imageUrlParse(response: Response): String {
+        throw Exception("Not used")
+    }
+
     private class GenreFilter : UriPartFilter(
         "题材",
         arrayOf(