Migrating 3 sources to kotlinx.serialization (#8463)

* HentaiHand: Migration to kotlinx.serialization

* SimplyHentai: Migration to kotlinx.serialization

* Kangaryu: Migration to kotlinx.serialization
This commit is contained in:
Arraiment 2021-08-08 23:38:14 +08:00 committed by GitHub
parent 5a0d513b94
commit 6470dd5245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 107 additions and 99 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 = 'HentaiHand' extName = 'HentaiHand'
pkgNameSuffix = 'all.hentaihand' pkgNameSuffix = 'all.hentaihand'
extClass = '.HentaiHandFactory' extClass = '.HentaiHandFactory'
extVersionCode = 3 extVersionCode = 4
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,17 +1,9 @@
package eu.kanade.tachiyomi.extension.all.hentaihand package eu.kanade.tachiyomi.extension.all.hentaihand
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.text.InputType import android.text.InputType
import android.widget.Toast import android.widget.Toast
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@ -24,20 +16,28 @@ 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.buildJsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale
@Nsfw @Nsfw
abstract class HentaiHand( abstract class HentaiHand(
@ -50,29 +50,37 @@ abstract class HentaiHand(
override val name: String = "HentaiHand$extraName" override val name: String = "HentaiHand$extraName"
override val supportsLatest = true override val supportsLatest = true
private val gson = Gson()
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor { authIntercept(it) } .addInterceptor { authIntercept(it) }
.build() .build()
private fun parseGenericResponse(response: Response): MangasPage { private val json: Json by injectLazy()
val data = gson.fromJson<JsonObject>(response.body!!.string())
return MangasPage( private fun slugToUrl(json: JsonObject) = json["slug"]!!.jsonPrimitive.content.prependIndent("/en/comic/")
data.getAsJsonArray("data").map {
SManga.create().apply { private fun jsonArrayToString(arrayKey: String, obj: JsonObject): String? {
url = "/en/comic/${it["slug"].asString}" val array = obj[arrayKey]!!.jsonArray
title = it["title"].asString if (array.isEmpty()) return null
thumbnail_url = it["thumb_url"].asString return array.joinToString(", ") {
} it.jsonObject["name"]!!.jsonPrimitive.content
}, }
!data["next_page_url"].isJsonNull
)
} }
// Popular // Popular
override fun popularMangaParse(response: Response): MangasPage = parseGenericResponse(response) override fun popularMangaParse(response: Response): MangasPage {
val jsonResponse = json.parseToJsonElement(response.body!!.string())
val mangaList = jsonResponse.jsonObject["data"]!!.jsonArray.map {
val obj = it.jsonObject
SManga.create().apply {
url = slugToUrl(obj)
title = obj["title"]!!.jsonPrimitive.content
thumbnail_url = obj["thumb_url"]!!.jsonPrimitive.content
}
}
val hasNextPage = jsonResponse.jsonObject["next_page_url"]!!.jsonPrimitive.content.isNotEmpty()
return MangasPage(mangaList, hasNextPage)
}
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val url = "$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all" val url = "$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all"
@ -81,7 +89,7 @@ abstract class HentaiHand(
// Latest // Latest
override fun latestUpdatesParse(response: Response): MangasPage = parseGenericResponse(response) override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week" val url = "$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week"
@ -90,17 +98,20 @@ abstract class HentaiHand(
// Search // Search
override fun searchMangaParse(response: Response): MangasPage = parseGenericResponse(response) override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
private fun lookupFilterId(query: String, uri: String): Int? { private fun lookupFilterId(query: String, uri: String): Int? {
// filter query needs to be resolved to an ID // filter query needs to be resolved to an ID
return client.newCall(GET("$baseUrl/api/$uri?q=$query")) return client.newCall(GET("$baseUrl/api/$uri?q=$query"))
.asObservableSuccess() .asObservableSuccess()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.map { .map { response ->
val data = gson.fromJson<JsonObject>(it.body!!.string()) // Returns the first matched id, or null if there are no results
// only the first tag will be used val idList = json.parseToJsonElement(response.body!!.string()).jsonObject["data"]!!.jsonArray.map {
data.getAsJsonArray("data").firstOrNull()?.let { t -> t["id"].asInt } it.jsonObject["id"]!!.jsonPrimitive.content
}
if (idList.isEmpty()) return@map null
else idList.first().toInt()
}.toBlocking().first() }.toBlocking().first()
} }
@ -138,12 +149,6 @@ abstract class HentaiHand(
// Details // Details
private fun tagArrayToString(array: JsonArray, key: String = "name"): String? {
if (array.size() == 0)
return null
return array.joinToString { it[key].asString }
}
private fun mangaDetailsApiRequest(manga: SManga): Request { private fun mangaDetailsApiRequest(manga: SManga): Request {
val slug = manga.url.removePrefix("/en/comic/") val slug = manga.url.removePrefix("/en/comic/")
return GET("$baseUrl/api/comics/$slug") return GET("$baseUrl/api/comics/$slug")
@ -156,28 +161,26 @@ abstract class HentaiHand(
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val data = gson.fromJson<JsonObject>(response.body!!.string()) val obj = json.parseToJsonElement(response.body!!.string()).jsonObject
return SManga.create().apply { return SManga.create().apply {
url = slugToUrl(obj)
artist = tagArrayToString(data.getAsJsonArray("artists")) title = obj["title"]!!.jsonPrimitive.content
author = tagArrayToString(data.getAsJsonArray("authors")) ?: artist thumbnail_url = obj["thumb_url"]!!.jsonPrimitive.content
artist = jsonArrayToString("artists", obj)
genre = listOf("tags", "relationships").map { author = jsonArrayToString("authors", obj) ?: artist
data.getAsJsonArray(it).map { t -> t["name"].asString } genre = listOfNotNull(jsonArrayToString("tags", obj), jsonArrayToString("relationships", obj)).joinToString(", ")
}.flatten().distinct().joinToString()
status = SManga.COMPLETED status = SManga.COMPLETED
description = listOf( description = listOf(
Pair("Alternative Title", data["alternative_title"].nullString), Pair("Alternative Title", obj["alternative_title"]!!.jsonPrimitive.content),
Pair("Groups", tagArrayToString(data.getAsJsonArray("groups"))), Pair("Groups", jsonArrayToString("groups", obj)),
Pair("Description", data["description"].nullString), Pair("Description", obj["description"]!!.jsonPrimitive.content),
Pair("Pages", data["pages"].asInt.toString()), Pair("Pages", obj["pages"]!!.jsonPrimitive.content),
Pair("Category", data["category"].nullObj?.get("name")?.asString), Pair("Category", obj["category"]!!.jsonObject["name"]!!.jsonPrimitive.content),
Pair("Language", data["language"].nullObj?.get("name")?.asString), Pair("Language", obj["language"]!!.jsonObject["name"]!!.jsonPrimitive.content),
Pair("Parodies", tagArrayToString(data.getAsJsonArray("parodies"))), Pair("Parodies", jsonArrayToString("parodies", obj)),
Pair("Characters", tagArrayToString(data.getAsJsonArray("characters"))) Pair("Characters", jsonArrayToString("characters", obj))
).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}:\n${it.second}" } ).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
} }
} }
@ -186,12 +189,12 @@ abstract class HentaiHand(
override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga) override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val data = gson.fromJson<JsonObject>(response.body!!.string()) val obj = json.parseToJsonElement(response.body!!.string()).jsonObject
return listOf( return listOf(
SChapter.create().apply { SChapter.create().apply {
url = "/en/comic/${data["slug"].asString}/reader/1" url = "/en/comic/${obj["slug"]!!.jsonPrimitive.content}/reader/1"
name = "Chapter" name = "Chapter"
date_upload = DATE_FORMAT.parse(data["uploaded_at"].asString)?.time ?: 0 date_upload = DATE_FORMAT.parse(obj["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0
chapter_number = 1f chapter_number = 1f
} }
) )
@ -204,12 +207,13 @@ abstract class HentaiHand(
return GET("$baseUrl/api/comics/$slug/images") return GET("$baseUrl/api/comics/$slug/images")
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> =
val data = gson.fromJson<JsonObject>(response.body!!.string()) json.parseToJsonElement(response.body!!.string()).jsonObject["images"]!!.jsonArray.map {
return data.getAsJsonArray("images").mapIndexed { i, it -> val imgObj = it.jsonObject
Page(i, "/en/comic/${data["comic"]["slug"].asString}/reader/${it["page"].asInt}", it["source_url"].asString) val index = imgObj["page"]!!.jsonPrimitive.int
val imgUrl = imgObj["source_url"]!!.jsonPrimitive.content
Page(index, "", imgUrl)
} }
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used") override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
@ -231,12 +235,12 @@ abstract class HentaiHand(
} }
private fun login(chain: Interceptor.Chain, username: String, password: String): String { private fun login(chain: Interceptor.Chain, username: String, password: String): String {
val jsonObject = JSONObject().apply { val jsonObject = buildJsonObject {
this.put("username", username) put("username", username)
this.put("password", password) put("password", password)
this.put("remember_me", true) put("remember_me", true)
} }
val body = RequestBody.create(MEDIA_TYPE, jsonObject.toString()) val body = jsonObject.toString().toRequestBody(MEDIA_TYPE)
val response = chain.proceed(POST("$baseUrl/api/login", headers, body)) val response = chain.proceed(POST("$baseUrl/api/login", headers, body))
if (response.code == 401) { if (response.code == 401) {
throw Exception("Failed to login, check if username and password are correct") throw Exception("Failed to login, check if username and password are correct")
@ -245,10 +249,9 @@ abstract class HentaiHand(
if (response.body == null) if (response.body == null)
throw Exception("Login response body is empty") throw Exception("Login response body is empty")
try { try {
return JSONObject(response.body!!.string()) // Returns access token as a string, unless unparseable
.getJSONObject("auth") return json.parseToJsonElement(response.body!!.string()).jsonObject["auth"]!!.jsonObject["access-token"]!!.jsonPrimitive.content
.getString("access_token") } catch (e: IllegalArgumentException) {
} catch (e: JSONException) {
throw Exception("Cannot parse login response body") throw Exception("Cannot parse login response body")
} }
} }
@ -357,8 +360,7 @@ abstract class HentaiHand(
) )
companion object { companion object {
@SuppressLint("SimpleDateFormat") private val DATE_FORMAT = SimpleDateFormat("yyyy-dd-MM", Locale.US)
private val DATE_FORMAT = SimpleDateFormat("yyyy-dd-MM")
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private const val USERNAME_TITLE = "Username" private const val USERNAME_TITLE = "Username"
private const val USERNAME_DEFAULT = "" private const val USERNAME_DEFAULT = ""

View File

@ -50,7 +50,7 @@ class HentaiHandFactory : SourceFactory {
) )
} }
class HentaiHandOther : HentaiHand("other", extraName = " (Unfiltered)") class HentaiHandOther : HentaiHand("all", extraName = " (Unfiltered)")
class HentaiHandEn : HentaiHand("en", 1) class HentaiHandEn : HentaiHand("en", 1)
class HentaiHandZh : HentaiHand("zh", 2) class HentaiHandZh : HentaiHand("zh", 2)
class HentaiHandJa : HentaiHand("ja", 3) class HentaiHandJa : HentaiHand("ja", 3)

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 = 'Simply Hentai' extName = 'Simply Hentai'
pkgNameSuffix = 'all.simplyhentai' pkgNameSuffix = 'all.simplyhentai'
extClass = '.SimplyHentaiFactory' extClass = '.SimplyHentaiFactory'
extVersionCode = 3 extVersionCode = 4
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,12 +1,6 @@
package eu.kanade.tachiyomi.extension.all.simplyhentai package eu.kanade.tachiyomi.extension.all.simplyhentai
import android.annotation.SuppressLint import android.annotation.SuppressLint
import com.github.salomonbrys.kotson.forEach
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -15,12 +9,16 @@ 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.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
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
abstract class SimplyHentai( abstract class SimplyHentai(
@ -36,7 +34,7 @@ abstract class SimplyHentai(
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val gson = Gson() private val json: Json by injectLazy()
// Popular // Popular
@ -158,8 +156,13 @@ abstract class SimplyHentai(
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
gson.fromJson<JsonObject>(response.body!!.string()).forEach { _, jsonElement -> json.parseToJsonElement(response.body!!.string()).jsonObject.forEach {
pages.add(Page(pages.size, "", jsonElement["sizes"]["full"].string)) pages.add(
Page(
pages.size, "",
it.value.jsonObject["sizes"]!!.jsonObject["full"]!!.jsonPrimitive.content
)
)
} }
return pages return pages

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 = 'Kangaryu' extName = 'Kangaryu'
pkgNameSuffix = 'fr.kangaryu' pkgNameSuffix = 'fr.kangaryu'
extClass = '.Kangaryu' extClass = '.Kangaryu'
extVersionCode = 2 extVersionCode = 3
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,11 +1,5 @@
package eu.kanade.tachiyomi.extension.fr.kangaryu package eu.kanade.tachiyomi.extension.fr.kangaryu
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -13,11 +7,16 @@ 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.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.json.Json
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.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
@ -87,14 +86,15 @@ class Kangaryu : ParsedHttpSource() {
return GET("$baseUrl/search?query=$query", headers) return GET("$baseUrl/search?query=$query", headers)
} }
private val gson by lazy { Gson() } private val json: Json by injectLazy()
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val mangas = gson.fromJson<JsonObject>(response.body!!.string())["suggestions"].array.map { json -> val mangas = json.parseToJsonElement(response.body!!.string()).jsonObject["suggestions"]!!.jsonArray.map {
val data = it.jsonObject["data"]!!.jsonPrimitive.content
SManga.create().apply { SManga.create().apply {
url = "/manga/${json["data"].string}" url = "/manga/$data"
title = json["value"].string title = it.jsonObject["value"]!!.jsonPrimitive.content
thumbnail_url = "https://kangaryu-team.fr/uploads/manga/${json["data"].string}/cover/cover_250x350.jpg" thumbnail_url = "https://kangaryu-team.fr/uploads/manga/$data/cover/cover_250x350.jpg"
} }
} }
return MangasPage(mangas, false) return MangasPage(mangas, false)