Replace org.json with kotlinx.serialization in some sources (#9569)

* Replace org.json in MeDocTruyenTranh.

* Replace org.json in Kuaikanmanhua.

* Replace org.json in QiXiManhua.

* Replace org.json in Desu.
This commit is contained in:
Alessandro Jean 2021-11-01 08:32:12 -03:00 committed by GitHub
parent d0e8f185ab
commit fd927c254d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 320 additions and 250 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Desu' extName = 'Desu'
pkgNameSuffix = 'ru.desu' pkgNameSuffix = 'ru.desu'
extClass = '.Desu' extClass = '.Desu'
extVersionCode = 12 extVersionCode = 13
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -9,12 +9,20 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.float
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.ArrayList import java.util.ArrayList
class Desu : HttpSource() { class Desu : HttpSource() {
@ -26,31 +34,34 @@ class Desu : HttpSource() {
override val supportsLatest = true override val supportsLatest = true
private val json: Json by injectLazy()
override fun headersBuilder() = Headers.Builder().apply { override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Tachiyomi") add("User-Agent", "Tachiyomi")
add("Referer", baseUrl) add("Referer", baseUrl)
} }
private fun mangaPageFromJSON(json: String, next: Boolean): MangasPage { private fun mangaPageFromJSON(jsonStr: String, next: Boolean): MangasPage {
val arr = JSONArray(json) val mangaList = json.parseToJsonElement(jsonStr).jsonArray
val ret = ArrayList<SManga>(arr.length()) .map {
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
ret.add(
SManga.create().apply { SManga.create().apply {
mangaFromJSON(obj, false) mangaFromJSON(it.jsonObject, false)
} }
) }
}
return MangasPage(ret, next) return MangasPage(mangaList, next)
} }
private fun SManga.mangaFromJSON(obj: JSONObject, chapter: Boolean) { private fun SManga.mangaFromJSON(obj: JsonObject, chapter: Boolean) {
val id = obj.getInt("id") val id = obj["id"]!!.jsonPrimitive.int
url = "/$id" url = "/$id"
title = obj.getString("name").split(" / ").first() title = obj["name"]!!.jsonPrimitive.content
thumbnail_url = obj.getJSONObject("image").getString("original") .split(" / ")
val ratingValue = obj.getString("score").toFloat() .first()
thumbnail_url = obj["image"]!!.jsonObject["original"]!!.jsonPrimitive.content
val ratingValue = obj["score"]!!.jsonPrimitive.floatOrNull ?: 0F
val ratingStar = when { val ratingStar = when {
ratingValue > 9.5 -> "★★★★★" ratingValue > 9.5 -> "★★★★★"
ratingValue > 8.5 -> "★★★★✬" ratingValue > 8.5 -> "★★★★✬"
@ -64,14 +75,13 @@ class Desu : HttpSource() {
ratingValue > 0.5 -> "✬☆☆☆☆" ratingValue > 0.5 -> "✬☆☆☆☆"
else -> "☆☆☆☆☆" else -> "☆☆☆☆☆"
} }
val rawAgeValue = obj.getString("adult")
val rawAgeStop = when (rawAgeValue) { val rawAgeStop = when (obj["adult"]!!.jsonPrimitive.int) {
"1" -> "18+" 1 -> "18+"
else -> "0+" else -> "0+"
} }
val rawTypeValue = obj.getString("kind") val rawTypeStr = when (obj["kind"]!!.jsonPrimitive.content) {
val rawTypeStr = when (rawTypeValue) {
"manga" -> "Манга" "manga" -> "Манга"
"manhwa" -> "Манхва" "manhwa" -> "Манхва"
"manhua" -> "Маньхуа" "manhua" -> "Маньхуа"
@ -81,21 +91,32 @@ class Desu : HttpSource() {
} }
var altName = "" var altName = ""
if (obj.getString("synonyms").isNotEmpty() && obj.getString("synonyms") != "null") {
altName = "Альтернативные названия:\n" + obj.getString("synonyms").replace("|", " / ") + "\n\n" if (obj["synonyms"]?.jsonPrimitive?.content.orEmpty().isNotEmpty()) {
altName = "Альтернативные названия:\n" +
obj["synonyms"]!!.jsonPrimitive.content
.replace("|", " / ") +
"\n\n"
} }
description = obj.getString("russian") + "\n" + ratingStar + " " + ratingValue + " (голосов: " + obj.getString("score_users") + ")\n" + altName + obj.getString("description")
description = obj["russian"]!!.jsonPrimitive.content + "\n" +
ratingStar + " " + ratingValue +
" (голосов: " +
obj["score_users"]!!.jsonPrimitive.int +
")\n" + altName +
obj["description"]!!.jsonPrimitive.content
genre = if (chapter) { genre = if (chapter) {
val jsonArray = obj.getJSONArray("genres") obj["genres"]!!.jsonArray
val genreList = mutableListOf<String>() .map { it.jsonObject["russian"]!!.jsonPrimitive.content }
for (i in 0 until jsonArray.length()) { .plusElement(rawTypeStr)
genreList.add(jsonArray.getJSONObject(i).getString("russian")) .plusElement(rawAgeStop)
} .joinToString()
genreList.plusElement(rawTypeStr).plusElement(rawAgeStop).joinToString()
} else { } else {
obj.getString("genres") + ", " + rawTypeStr + ", " + rawAgeStop obj["genres"]!!.jsonPrimitive.content + ", " + rawTypeStr + ", " + rawAgeStop
} }
status = when (obj.getString("status")) {
status = when (obj["status"]!!.jsonPrimitive.content) {
"ongoing" -> SManga.ONGOING "ongoing" -> SManga.ONGOING
"released" -> SManga.COMPLETED "released" -> SManga.COMPLETED
"copyright" -> SManga.LICENSED "copyright" -> SManga.LICENSED
@ -103,11 +124,13 @@ class Desu : HttpSource() {
} }
} }
override fun popularMangaRequest(page: Int) = GET("$baseUrl$API_URL/?limit=50&order=popular&page=$page") override fun popularMangaRequest(page: Int) =
GET("$baseUrl$API_URL/?limit=50&order=popular&page=$page")
override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl$API_URL/?limit=50&order=updated&page=$page") override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl$API_URL/?limit=50&order=updated&page=$page")
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
@ -136,12 +159,13 @@ class Desu : HttpSource() {
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val res = response.body!!.string() val res = json.parseToJsonElement(response.body!!.string()).jsonObject
val obj = JSONObject(res).getJSONArray("response") val obj = res["response"]!!.jsonArray
val nav = JSONObject(res).getJSONObject("pageNavParams") val nav = res["pageNavParams"]!!.jsonObject
val count = nav.getInt("count") val count = nav["count"]!!.jsonPrimitive.int
val limit = nav.getInt("limit") val limit = nav["limit"]!!.jsonPrimitive.int
val page = nav.getInt("page") val page = nav["page"]!!.jsonPrimitive.int
return mangaPageFromJSON(obj.toString(), count > page * limit) return mangaPageFromJSON(obj.toString(), count > page * limit)
} }
@ -162,53 +186,56 @@ class Desu : HttpSource() {
return GET(baseUrl + "/manga" + manga.url, headers) return GET(baseUrl + "/manga" + manga.url, headers)
} }
override fun mangaDetailsParse(response: Response) = SManga.create().apply { override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val obj = JSONObject(response.body!!.string()).getJSONObject("response") val obj = json.parseToJsonElement(response.body!!.string())
.jsonObject["response"]!!
.jsonObject
mangaFromJSON(obj, true) mangaFromJSON(obj, true)
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val obj = JSONObject(response.body!!.string()).getJSONObject("response") val obj = json.parseToJsonElement(response.body!!.string())
val ret = ArrayList<SChapter>() .jsonObject["response"]!!
.jsonObject
val cid = obj.getInt("id") val cid = obj["id"]!!.jsonPrimitive.int
val arr = obj.getJSONObject("chapters").getJSONArray("list") return obj["chapters"]!!.jsonObject["list"]!!.jsonArray.map {
for (i in 0 until arr.length()) { val chapterObj = it.jsonObject
val obj2 = arr.getJSONObject(i) val ch = chapterObj["ch"]!!.jsonPrimitive.float
ret.add( val fullNumStr = "${chapterObj["vol"]!!.jsonPrimitive.int} . Глава $ch"
SChapter.create().apply { val title = chapterObj["title"]?.jsonPrimitive?.content.orEmpty()
val ch = obj2.getString("ch")
val fullnumstr = obj2.getString("vol") + ". " + "Глава " + ch SChapter.create().apply {
val title = if (obj2.getString("title") == "null") "" else obj2.getString("title") name = if (title.isEmpty()) {
name = if (title.isEmpty()) { fullNumStr
fullnumstr } else {
} else { "$fullNumStr: $title"
"$fullnumstr: $title"
}
val id = obj2.getString("id")
url = "/$cid/chapter/$id"
chapter_number = ch.toFloat()
date_upload = obj2.getLong("date") * 1000
} }
) url = "/$cid/chapter/${chapterObj["id"]!!.jsonPrimitive.int}"
chapter_number = ch
date_upload = chapterObj["date"]!!.jsonPrimitive.long * 1000L
}
} }
return ret
} }
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + API_URL + manga.url, headers) return GET(baseUrl + API_URL + manga.url, headers)
} }
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + API_URL + chapter.url, headers) return GET(baseUrl + API_URL + chapter.url, headers)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val obj = JSONObject(response.body!!.string()).getJSONObject("response") val obj = json.parseToJsonElement(response.body!!.string())
val pages = obj.getJSONObject("pages") .jsonObject["response"]!!
val list = pages.getJSONArray("list") .jsonObject
val ret = ArrayList<Page>(list.length())
for (i in 0 until list.length()) { return obj["pages"]!!.jsonObject["list"]!!.jsonArray
ret.add(Page(i, "", list.getJSONObject(i).getString("img"))) .mapIndexed { i, jsonEl ->
} Page(i, "", jsonEl.jsonObject["img"]!!.jsonPrimitive.content)
return ret }
} }
override fun imageUrlParse(response: Response) = override fun imageUrlParse(response: Response) =
@ -236,10 +263,6 @@ class Desu : HttpSource() {
} }
} }
} }
companion object {
const val PREFIX_SLUG_SEARCH = "slug:"
private const val API_URL = "/manga/api"
}
private class OrderBy : Filter.Select<String>( private class OrderBy : Filter.Select<String>(
"Сортировка", "Сортировка",
@ -247,10 +270,13 @@ class Desu : HttpSource() {
) )
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанр", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанр", genres)
private class TypeList(types: List<Type>) : Filter.Group<Type>("Тип", types) private class TypeList(types: List<Type>) : Filter.Group<Type>("Тип", types)
private class Type(name: String, val id: String) : Filter.CheckBox(name) private class Type(name: String, val id: String) : Filter.CheckBox(name)
private class Genre(name: String, val id: String) : Filter.CheckBox(name) private class Genre(name: String, val id: String) : Filter.CheckBox(name)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
OrderBy(), OrderBy(),
TypeList(getTypeList()), TypeList(getTypeList()),
@ -312,4 +338,10 @@ class Desu : HttpSource() {
Genre("Юри", "Yuri"), Genre("Юри", "Yuri"),
Genre("Яой", "Yaoi") Genre("Яой", "Yaoi")
) )
companion object {
const val PREFIX_SLUG_SEARCH = "slug:"
private const val API_URL = "/manga/api"
}
} }

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'MeDocTruyenTranh' extName = 'MeDocTruyenTranh'
pkgNameSuffix = 'vi.medoctruyentranh' pkgNameSuffix = 'vi.medoctruyentranh'
extClass = '.MeDocTruyenTranh' extClass = '.MeDocTruyenTranh'
extVersionCode = 4 extVersionCode = 5
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -8,12 +8,15 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -29,6 +32,8 @@ class MeDocTruyenTranh : ParsedHttpSource() {
override val client = network.cloudflareClient override val client = network.cloudflareClient
private val json: Json by injectLazy()
override fun popularMangaSelector() = "div.classifyList a" override fun popularMangaSelector() = "div.classifyList a"
override fun searchMangaSelector() = ".listCon a" override fun searchMangaSelector() = ".listCon a"
@ -37,37 +42,24 @@ class MeDocTruyenTranh : ParsedHttpSource() {
return GET("$baseUrl/tim-truyen/toan-bo" + if (page > 1) "/$page" else "", headers) return GET("$baseUrl/tim-truyen/toan-bo" + if (page > 1) "/$page" else "", headers)
} }
private inline fun <reified T, R> JSONArray.mapJSONArray(transform: (Int, T) -> R): List<R> {
val list = mutableListOf<R>()
for (i in 0 until this.length()) {
list.add(transform(i, this[i] as T))
}
return list
}
private fun JSONArray.flatten(): JSONArray {
val list = mutableListOf<Any>()
for (i in 0 until this.length()) {
val childArray = this[i] as JSONArray
for (j in 0 until childArray.length()) {
list.add(childArray[j])
}
}
return JSONArray(list)
}
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
// trying to build URLs from this JSONObject could cause issues but we need it to get thumbnails // trying to build URLs from this JSONObject could cause issues but we need it to get thumbnails
val titleCoverMap = JSONObject(document.select("script#__NEXT_DATA__").first().data()) val nextData = document.select("script#__NEXT_DATA__").first()
.getJSONObject("props") .let { json.parseToJsonElement(it.data()).jsonObject }
.getJSONObject("pageProps")
.getJSONObject("initialState") val titleCoverMap = nextData["props"]!!
.getJSONObject("classify") .jsonObject["pageProps"]!!
.getJSONArray("comics") .jsonObject["initialState"]!!
.mapJSONArray { _, jsonObject: JSONObject -> .jsonObject["classify"]!!
Pair(jsonObject.getString("title"), jsonObject.getString("coverimg")) .jsonObject["comics"]!!
.jsonArray
.map {
Pair(
it.jsonObject["title"]!!.jsonPrimitive.content,
it.jsonObject["coverimg"]!!.jsonPrimitive.content
)
} }
.toMap() .toMap()
@ -79,6 +71,7 @@ class MeDocTruyenTranh : ParsedHttpSource() {
return MangasPage(mangas, document.select(popularMangaNextPageSelector()) != null) return MangasPage(mangas, document.select(popularMangaNextPageSelector()) != null)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/search/$query", headers) return GET("$baseUrl/search/$query", headers)
} }
@ -108,26 +101,24 @@ class MeDocTruyenTranh : ParsedHttpSource() {
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
val manga = SManga.create()
val jsonData = JSONObject(document.select("#__NEXT_DATA__").first()!!.data()) val nextData = document.select("script#__NEXT_DATA__").first()
val mangaDetail = jsonData .let { json.parseToJsonElement(it.data()).jsonObject }
.getJSONObject("props")
.getJSONObject("pageProps") val mangaDetail = nextData["props"]!!
.getJSONObject("initialState") .jsonObject["pageProps"]!!
.getJSONObject("detail") .jsonObject["initialState"]!!
.getJSONObject("story_item") .jsonObject["detail"]!!
manga.title = mangaDetail.getString("title") .jsonObject["story_item"]!!
manga.author = mangaDetail.getJSONArray("author_list").getString(0) .jsonObject
val genres = mutableListOf<String>()
for (i in 0 until mangaDetail.getJSONArray("category_list").length()) { title = mangaDetail["title"]!!.jsonPrimitive.content
genres.add(mangaDetail.getJSONArray("category_list").getString(i)) author = mangaDetail["author_list"]!!.jsonArray.joinToString { it.jsonPrimitive.content }
} genre = mangaDetail["category_list"]!!.jsonArray.joinToString { it.jsonPrimitive.content }
manga.genre = genres.joinToString(", ") description = mangaDetail["summary"]!!.jsonPrimitive.content
manga.description = mangaDetail.getString("summary") status = parseStatus(mangaDetail["is_updating"]!!.jsonPrimitive.content)
manga.status = parseStatus(mangaDetail.getString("is_updating")) thumbnail_url = mangaDetail["coverimg"]!!.jsonPrimitive.content
manga.thumbnail_url = mangaDetail.getString("coverimg")
return manga
} }
private fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {
@ -139,18 +130,23 @@ class MeDocTruyenTranh : ParsedHttpSource() {
override fun chapterListSelector() = "div.chapters a" override fun chapterListSelector() = "div.chapters a"
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
return JSONObject(response.asJsoup().select("script#__NEXT_DATA__").first().data()) val nextData = response.asJsoup().select("script#__NEXT_DATA__").first()
.getJSONObject("props") .let { json.parseToJsonElement(it.data()).jsonObject }
.getJSONObject("pageProps")
.getJSONObject("initialState") return nextData["props"]!!
.getJSONObject("detail") .jsonObject["pageProps"]!!
.getJSONArray("story_chapters") .jsonObject["initialState"]!!
.flatten() .jsonObject["detail"]!!
.mapJSONArray { _, jsonObject: JSONObject -> .jsonObject["story_chapters"]!!
.jsonArray
.flatMap { it.jsonArray }
.map {
val chapterObj = it.jsonObject
SChapter.create().apply { SChapter.create().apply {
name = jsonObject.getString("title") name = chapterObj["title"]!!.jsonPrimitive.content
setUrlWithoutDomain("${response.request.url}/${jsonObject.getString("chapter_index")}") date_upload = parseChapterDate(chapterObj["time"]!!.jsonPrimitive.content)
date_upload = parseChapterDate(jsonObject.getString("time")) setUrlWithoutDomain("${response.request.url}/${chapterObj["chapter_index"]!!.jsonPrimitive.content}")
} }
} }
.reversed() .reversed()
@ -166,15 +162,19 @@ class MeDocTruyenTranh : ParsedHttpSource() {
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return JSONObject(document.select("#__NEXT_DATA__").first()?.data() ?: "{}") val nextData = document.select("script#__NEXT_DATA__").firstOrNull()
.getJSONObject("props") ?.let { json.parseToJsonElement(it.data()).jsonObject }
.getJSONObject("pageProps") ?: return emptyList()
.getJSONObject("initialState")
.getJSONObject("read") return nextData["props"]!!
.getJSONObject("detail_item") .jsonObject["pageProps"]!!
.getJSONArray("elements") .jsonObject["initialState"]!!
.mapJSONArray { i, jsonObject: JSONObject -> .jsonObject["read"]!!
Page(i, "", jsonObject.getString("content")) .jsonObject["detail_item"]!!
.jsonObject["elements"]!!
.jsonArray
.mapIndexed { i, jsonEl ->
Page(i, "", jsonEl.jsonObject["content"]!!.jsonPrimitive.content)
} }
} }

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Kuaikanmanhua' extName = 'Kuaikanmanhua'
pkgNameSuffix = 'zh.kuaikanmanhua' pkgNameSuffix = 'zh.kuaikanmanhua'
extClass = '.Kuaikanmanhua' extClass = '.Kuaikanmanhua'
extVersionCode = 5 extVersionCode = 6
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -10,14 +10,20 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class Kuaikanmanhua : HttpSource() { class Kuaikanmanhua : HttpSource() {
@ -34,6 +40,8 @@ class Kuaikanmanhua : HttpSource() {
private val apiUrl = "https://api.kkmh.com" private val apiUrl = "https://api.kkmh.com"
private val json: Json by injectLazy()
// Popular // Popular
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
@ -42,25 +50,25 @@ class Kuaikanmanhua : HttpSource() {
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val body = response.body!!.string() val body = response.body!!.string()
val jsonList = JSONObject(body).getJSONObject("data").getJSONArray("topics") val jsonList = json.parseToJsonElement(body).jsonObject["data"]!!
.jsonObject["topics"]!!
.jsonArray
return parseMangaJsonArray(jsonList) return parseMangaJsonArray(jsonList)
} }
private fun parseMangaJsonArray(jsonList: JSONArray, isSearch: Boolean = false): MangasPage { private fun parseMangaJsonArray(jsonList: JsonArray, isSearch: Boolean = false): MangasPage {
val mangaList = mutableListOf<SManga>() val mangaList = jsonList.map {
val mangaObj = it.jsonObject
for (i in 0 until jsonList.length()) { SManga.create().apply {
val obj = jsonList.getJSONObject(i) title = mangaObj["title"]!!.jsonPrimitive.content
mangaList.add( thumbnail_url = mangaObj["vertical_image_url"]!!.jsonPrimitive.content
SManga.create().apply { url = "/web/topic/" + mangaObj["id"]!!.jsonPrimitive.int
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 // KKMH does not have pages when you search
return MangasPage(mangaList, mangaList.size > 9 && !isSearch) return MangasPage(mangaList, hasNextPage = mangaList.size > 9 && !isSearch)
} }
// Latest // Latest
@ -110,12 +118,12 @@ class Kuaikanmanhua : HttpSource() {
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val body = response.body!!.string() val body = response.body!!.string()
val jsonObj = JSONObject(body).getJSONObject("data") val jsonObj = json.parseToJsonElement(body).jsonObject["data"]!!.jsonObject
if (jsonObj.has("hit")) { if (jsonObj["hit"] != null) {
return parseMangaJsonArray(jsonObj.getJSONArray("hit"), true) return parseMangaJsonArray(jsonObj["hit"]!!.jsonArray, true)
} }
return parseMangaJsonArray(jsonObj.getJSONArray("topics"), false) return parseMangaJsonArray(jsonObj["topics"]!!.jsonArray, false)
} }
// Details // Details
@ -128,32 +136,37 @@ class Kuaikanmanhua : HttpSource() {
return Observable.just(sManga) return Observable.just(sManga)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val data = JSONObject(response.body!!.string()).getJSONObject("data") val data = json.parseToJsonElement(response.body!!.string())
val manga = SManga.create() .jsonObject["data"]!!
manga.title = data.getString("title") .jsonObject
manga.thumbnail_url = data.getString("vertical_image_url")
manga.author = data.getJSONObject("user").getString("nickname")
manga.description = data.getString("description")
manga.status = data.getInt("update_status_code")
return manga title = data["title"]!!.jsonPrimitive.content
thumbnail_url = data["vertical_image_url"]!!.jsonPrimitive.content
author = data["user"]!!.jsonObject["nickname"]!!.jsonPrimitive.content
description = data["description"]!!.jsonPrimitive.content
status = data["update_status_code"]!!.jsonPrimitive.int
} }
// Chapters & Pages // Chapters & Pages
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
val chapters = mutableListOf<SChapter>()
val script = document.select("script:containsData(comics)").first().data() val script = document.select("script:containsData(comics)").first().data()
val comics = JSONArray(script.substringAfter("comics:").substringBefore(",first_comic_id")) val comics = script.substringAfter("comics:")
val variable = script.substringAfter("(function(").substringBefore("){").split(",") .substringBefore(",first_comic_id")
val value = script.substringAfterLast("}}(").substringBefore("));").split(",") .let { json.parseToJsonElement(it).jsonArray }
val variable = script.substringAfter("(function(")
.substringBefore("){")
.split(",")
val value = script.substringAfterLast("}}(")
.substringBefore("));")
.split(",")
document.select("div.TopicItem").forEachIndexed { index, element -> return document.select("div.TopicItem")
chapters.add( .mapIndexed { index, element ->
SChapter.create().apply { SChapter.create().apply {
val idVar = comics.getJSONObject(index).getString("id") val idVar = comics[index].jsonObject["id"]!!.jsonPrimitive.content
val id = value[variable.indexOf(idVar)] val id = value[variable.indexOf(idVar)]
url = "/web/comic/$id" url = "/web/comic/$id"
name = element.select("div.title > a").text() name = element.select("div.title > a").text()
@ -167,11 +180,10 @@ class Kuaikanmanhua : HttpSource() {
} else { } else {
"20$dateStr" "20$dateStr"
} }
date_upload = SimpleDateFormat("yyyy-MM-dd").parse(dateStr).time date_upload = runCatching { DATE_FORMAT.parse(dateStr)?.time }
.getOrNull() ?: 0L
} }
)
} }
return chapters
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
@ -187,18 +199,26 @@ class Kuaikanmanhua : HttpSource() {
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val pages = ArrayList<Page>()
val document = response.asJsoup() val document = response.asJsoup()
val script = document.selectFirst("script:containsData(comicImages)").data() val script = document.selectFirst("script:containsData(comicImages)").data()
val images = JSONArray(script.substringAfter("comicImages:").substringBefore("},nextComicInfo")) val images = script.substringAfter("comicImages:")
val variable = script.substringAfter("(function(").substringBefore("){").split(",") .substringBefore("},nextComicInfo")
val value = script.substringAfterLast("}}(").substringBefore("));").split(",") .let { json.parseToJsonElement(it).jsonArray }
for (i in 0 until images.length()) { val variable = script.substringAfter("(function(")
val urlVar = images.getJSONObject(i).getString("url") .substringBefore("){")
val url = value[variable.indexOf(urlVar)].replace("\\u002F", "/").replace("\"", "") .split(",")
pages.add(Page(i, "", url)) val value = script.substringAfterLast("}}(")
.substringBefore("));")
.split(",")
return images.mapIndexed { index, jsonEl ->
val urlVar = jsonEl.jsonObject["url"]!!.jsonPrimitive.content
val imageUrl = value[variable.indexOf(urlVar)]
.replace("\\u002F", "/")
.replace("\"", "")
Page(index, "", imageUrl)
} }
return pages
} }
// Filters // Filters
@ -256,5 +276,9 @@ class Kuaikanmanhua : HttpSource() {
companion object { companion object {
const val TOPIC_ID_SEARCH_PREFIX = "topic:" const val TOPIC_ID_SEARCH_PREFIX = "topic:"
private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
} }
} }

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'QiXiManhua' extName = 'QiXiManhua'
pkgNameSuffix = 'zh.qiximh' pkgNameSuffix = 'zh.qiximh'
extClass = '.Qiximh' extClass = '.Qiximh'
extVersionCode = 2 extVersionCode = 3
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -11,19 +11,30 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONArray import uy.kohesive.injekt.injectLazy
import org.json.JSONObject
class Qiximh : HttpSource() { class Qiximh : HttpSource() {
override val lang = "zh" override val lang = "zh"
override val supportsLatest = true override val supportsLatest = true
override val name = "七夕漫画" override val name = "七夕漫画"
override val baseUrl = "http://qiximh1.com" override val baseUrl = "http://qiximh1.com"
// This is hard limit by API // This is hard limit by API
val maxPage = 5 private val maxPage = 5
private val json: Json by injectLazy()
// Used in Rank API // Used in Rank API
private enum class RANKTYPE(val rankVal: Int) { private enum class RANKTYPE(val rankVal: Int) {
@ -84,20 +95,19 @@ class Qiximh : HttpSource() {
} }
private fun commonDataProcess(origRequest: Request, responseBody: String): MangasPage { private fun commonDataProcess(origRequest: Request, responseBody: String): MangasPage {
val jsonData = JSONArray(responseBody) val jsonData = json.parseToJsonElement(responseBody).jsonArray
val mangaArr = mutableListOf<SManga>() val mangaArr = jsonData.map {
val targetObj = it.jsonObject
for (i in 0 until jsonData.length()) { SManga.create().apply {
val targetObj = jsonData.getJSONObject(i) title = targetObj["name"]!!.jsonPrimitive.content
mangaArr.add( status = SManga.UNKNOWN
SManga.create().apply { thumbnail_url = targetObj["imgurl"]!!.jsonPrimitive.content
title = targetObj.get("name") as String // Extension is wrongly adding the baseURL to the SManga.
status = SManga.UNKNOWN // I kept it as it is to avoid user migrations.
thumbnail_url = targetObj.get("imgurl") as String url = "$baseUrl/${targetObj["id"]!!.jsonPrimitive.int}"
url = "$baseUrl/${targetObj.get("id")}/" }
}
)
} }
val requestBody = origRequest.body as FormBody val requestBody = origRequest.body as FormBody
@ -153,36 +163,37 @@ class Qiximh : HttpSource() {
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val responseBody = response.body val responseBody = response.body
val mangaArr = mutableListOf<SManga>() ?: return MangasPage(emptyList(), false)
if (responseBody != null) { val responseString = responseBody.string()
val responseString = responseBody.string()
if (!responseString.isNullOrEmpty()) {
if (responseString.startsWith("[")) {
// This is to process filter
return commonDataProcess(response.request, responseString)
} else {
val jsonData = JSONObject(responseString)
if (jsonData.get("msg") == "success") {
val jsonArr = jsonData.getJSONArray("search_data")
for (i in 0 until jsonArr.length()) { if (responseString.isNotEmpty()) {
val targetObj = jsonArr.getJSONObject(i) if (responseString.startsWith("[")) {
mangaArr.add( // This is to process filter
SManga.create().apply { return commonDataProcess(response.request, responseString)
title = targetObj.get("name") as String } else {
thumbnail_url = targetObj.get("imgs") as String val jsonData = json.parseToJsonElement(responseString).jsonObject
url = "$baseUrl/${targetObj.get("id")}/"
} if (jsonData["msg"]!!.jsonPrimitive.content == "success") {
) val mangaArr = jsonData["search_data"]!!.jsonArray.map {
val targetObj = it.jsonObject
SManga.create().apply {
title = targetObj["name"]!!.jsonPrimitive.content
thumbnail_url = targetObj["imgs"]!!.jsonPrimitive.content
// Extension is wrongly adding the baseURL to the SManga.
// I kept it as it is to avoid user migrations.
url = "$baseUrl/${targetObj["id"]!!.jsonPrimitive.int}"
} }
} }
return MangasPage(mangaArr, false)
} }
} }
} }
// Search does not have pagination // Search does not have pagination
return MangasPage(mangaArr, false) return MangasPage(emptyList(), false)
} }
// Filter // Filter
@ -247,13 +258,16 @@ class Qiximh : HttpSource() {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
// API does not allow retrieve full chapter list, hence the need to parse the chapters from both HTML and API // API does not allow retrieve full chapter list, hence the need to parse
val htmlChapters = document.select(".catalog_list.row_catalog_list a").map { // the chapters from both HTML and API
SChapter.create().apply { val chapterList = document.select(".catalog_list.row_catalog_list a")
name = it.text() .map {
url = "$baseUrl${it.attr("href")}" SChapter.create().apply {
name = it.text()
url = "$baseUrl${it.attr("href")}"
}
} }
} .toMutableList()
val mangaUrl = response.request.url.toString() val mangaUrl = response.request.url.toString()
@ -267,22 +281,18 @@ class Qiximh : HttpSource() {
) )
val inlineResponse = client.newCall(request).execute() val inlineResponse = client.newCall(request).execute()
val jsonData = JSONArray(inlineResponse.body!!.string()) val jsonData = json.parseToJsonElement(inlineResponse.body!!.string()).jsonArray
val chapterArr = mutableListOf<SChapter>() chapterList += jsonData.map {
chapterArr.addAll(htmlChapters) val targetObj = it.jsonObject
for (i in 0 until jsonData.length()) { SChapter.create().apply {
val targetObj = jsonData.getJSONObject(i) name = targetObj["chaptername"]!!.jsonPrimitive.content
chapterArr.add( url = "$mangaUrl${targetObj["chapterid"]!!.jsonPrimitive.int}.html"
SChapter.create().apply { }
name = targetObj.get("chaptername") as String
url = "$mangaUrl${targetObj.get("chapterid")}.html"
}
)
} }
return chapterArr return chapterList
} }
// Page // Page