MangaMutiny - Migration from Gson to kotlinx.serialization (#7401)

* Initial serialization with kotlinx.serialization draft

* Serialization without Serializable
This commit is contained in:
E3FxGaming 2021-06-03 12:22:43 +02:00 committed by GitHub
parent 897a5d94ba
commit 8dce249839
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 209 additions and 156 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Manga Mutiny'
pkgNameSuffix = "en.mangamutiny"
extClass = '.MangaMutiny'
extVersionCode = 7
extVersionCode = 8
libVersion = '1.2'
containsNsfw = true
}

View File

@ -1,9 +1,6 @@
package eu.kanade.tachiyomi.extension.en.mangamutiny
import android.net.Uri
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
@ -13,28 +10,12 @@ 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.HttpSource
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
fun JsonObject.getNullable(key: String): JsonElement? {
val value: JsonElement = this.get(key) ?: return null
if (value.isJsonNull) {
return null
}
return value
}
fun Float.toStringWithoutDotZero(): String = when (this % 1) {
0F -> this.toInt().toString()
else -> this.toString()
}
import uy.kohesive.injekt.injectLazy
class MangaMutiny : HttpSource() {
@ -45,7 +26,7 @@ class MangaMutiny : HttpSource() {
override val lang = "en"
private val parser = JsonParser()
private val json: Json by injectLazy()
private val baseUrlAPI = "https://api.mangamutiny.org"
@ -78,64 +59,15 @@ class MangaMutiny : HttpSource() {
mangaDetailsRequestCommon(manga, false)
override fun chapterListParse(response: Response): List<SChapter> {
val chapterList = mutableListOf<SChapter>()
val responseBody = response.body
if (responseBody != null) {
val jsonChapters = JsonParser().parse(responseBody.charStream()).asJsonObject
.get("chapters").asJsonArray
for (singleChapterJsonElement in jsonChapters) {
val singleChapterJsonObject = singleChapterJsonElement.asJsonObject
chapterList.add(
SChapter.create().apply {
name = chapterTitleBuilder(singleChapterJsonObject)
url = singleChapterJsonObject.get("slug").asString
date_upload = parseDate(singleChapterJsonObject.get("releasedAt").asString)
chapterNumberBuilder(singleChapterJsonObject)?.let { chapterNumber ->
chapter_number = chapterNumber
}
}
)
return responseBody?.use {
json.decodeFromString(ListChapterDS, it.string()).also {
responseBody.close()
}
responseBody.close()
}
return chapterList
} ?: listOf()
}
private fun chapterNumberBuilder(rootNode: JsonObject): Float? =
rootNode.getNullable("chapter")?.asFloat
private fun chapterTitleBuilder(rootNode: JsonObject): String {
val volume = rootNode.getNullable("volume")?.asInt
val chapter = rootNode.getNullable("chapter")?.asFloat?.toStringWithoutDotZero()
val textTitle = rootNode.getNullable("title")?.asString
val chapterTitle = StringBuilder()
if (volume != null) chapterTitle.append("Vol. $volume")
if (chapter != null) {
if (volume != null) chapterTitle.append(" ")
chapterTitle.append("Chapter $chapter")
}
if (textTitle != null && textTitle != "") {
if (volume != null || chapter != null) chapterTitle.append(": ")
chapterTitle.append(textTitle)
}
return chapterTitle.toString()
}
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
private fun parseDate(dateAsString: String): Long =
dateFormatter.parse(dateAsString)?.time ?: 0
// latest
override fun latestUpdatesRequest(page: Int): Request =
mangaRequest(page, filters = FilterList(SortFilter().apply { this.state = 1 }))
@ -162,32 +94,15 @@ class MangaMutiny : HttpSource() {
}
override fun mangaDetailsParse(response: Response): SManga {
val manga = SManga.create()
val responseBody = response.body
if (responseBody != null) {
val rootNode = parser.parse(responseBody.charStream()).asJsonObject
manga.apply {
status = when (rootNode.get("status").asString) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
description = rootNode.getNullable("summary")?.asString
thumbnail_url = rootNode.getNullable("thumbnail")?.asString
title = rootNode.get("title").asString
url = rootNode.get("slug").asString
artist = rootNode.getNullable("artists")?.asString
author = rootNode.get("authors").asString
genre = rootNode.get("tags").asJsonArray
.joinToString { singleGenre -> singleGenre.asString }
return responseBody.use {
json.decodeFromString(SMangaDS, it.string())
}
responseBody.close()
} else {
throw IllegalStateException("Response code ${response.code}")
}
return manga
}
override fun pageListRequest(chapter: SChapter): Request {
@ -199,31 +114,11 @@ class MangaMutiny : HttpSource() {
}
override fun pageListParse(response: Response): List<Page> {
val pageList = ArrayList<Page>()
val responseBody = response.body
if (responseBody != null) {
val rootNode = parser.parse(responseBody.charStream()).asJsonObject
// Build chapter url for every image of this chapter
val storageLocation = rootNode.get("storage").asString
val manga = rootNode.get("manga").asString
val chapterId = rootNode.get("id").asString
val chapterUrl = "$storageLocation/$manga/$chapterId/"
// Process every image of this chapter
val images = rootNode.get("images").asJsonArray
for (i in 0 until images.size()) {
pageList.add(Page(i, "", chapterUrl + images[i].asString))
}
responseBody.close()
}
return pageList
return responseBody?.use {
json.decodeFromString(ListPageDS, it.string())
} ?: listOf()
}
// Search
@ -234,46 +129,20 @@ class MangaMutiny : HttpSource() {
// commonly used functions
private fun mangaParse(response: Response): MangasPage {
val mangasPage = ArrayList<SManga>()
val responseBody = response.body
var totalObjects = 0
return if (responseBody != null) {
val deserializationResult = json.decodeFromString(PageInfoDS, responseBody.string())
val totalObjects = deserializationResult.second
val skipped = response.request.url.queryParameter("skip")?.toInt() ?: 0
val moreElementsToSkip = skipped + fetchAmount < totalObjects
val pageSizeEqualsFetchAmount = deserializationResult.first.size == fetchAmount
val hasMorePages = pageSizeEqualsFetchAmount && moreElementsToSkip
if (responseBody != null) {
val rootNode = parser.parse(responseBody.charStream())
if (rootNode.isJsonObject) {
val rootObject = rootNode.asJsonObject
val itemsArray = rootObject.get("items").asJsonArray
for (singleItem in itemsArray) {
val mangaObject = singleItem.asJsonObject
mangasPage.add(
SManga.create().apply {
this.title = mangaObject.get("title").asString
this.thumbnail_url = mangaObject.getNullable("thumbnail")?.asString
this.url = mangaObject.get("slug").asString
}
)
}
// total number of manga the server found in its database
// and is returning paginated page by page:
totalObjects = rootObject.getNullable("total")?.asInt ?: 0
}
responseBody.close()
MangasPage(deserializationResult.first, hasMorePages)
} else {
MangasPage(listOf(), false)
}
val skipped = response.request.url.queryParameter("skip")?.toInt() ?: 0
val moreElementsToSkip = skipped + fetchAmount < totalObjects
val pageSizeEqualsFetchAmount = mangasPage.size == fetchAmount
val hasMorePages = pageSizeEqualsFetchAmount && moreElementsToSkip
return MangasPage(mangasPage, hasMorePages)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {

View File

@ -0,0 +1,183 @@
package eu.kanade.tachiyomi.extension.en.mangamutiny
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.float
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
private fun JsonElement?.primitiveContent(): String? {
if (this is JsonNull) return null
return this?.jsonPrimitive?.content
}
private fun JsonElement?.primitiveInt(): Int? {
if (this is JsonNull) return null
return this?.jsonPrimitive?.int
}
private fun JsonElement?.primitiveFloat(): Float? {
if (this is JsonNull) return null
return this?.jsonPrimitive?.float
}
private val jsonObjectToMapSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
object PageInfoDS : DeserializationStrategy<Pair<List<SManga>, Int>> {
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
override fun deserialize(decoder: Decoder): Pair<List<SManga>, Int> {
require(decoder is JsonDecoder)
val json = decoder.json
val jsonElement = decoder.decodeJsonElement()
require(jsonElement is JsonObject)
val items = (jsonElement["items"] as JsonArray).map { json.decodeFromJsonElement(SMangaDS, it) }
val total = jsonElement["total"]?.jsonPrimitive?.int
require(total != null)
return Pair(items, total)
}
}
object SMangaDS : DeserializationStrategy<SManga> {
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
override fun deserialize(decoder: Decoder): SManga {
require(decoder is JsonDecoder)
val jsonElement = decoder.decodeJsonElement()
require(jsonElement is JsonObject)
val title = jsonElement["title"].primitiveContent()
val slug = jsonElement["slug"].primitiveContent()
val thumbnail = jsonElement["thumbnail"].primitiveContent()
val status: Int = jsonElement["status"].primitiveContent()?.let {
when (it) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
} ?: SManga.UNKNOWN
val summary: String? = jsonElement["summary"].primitiveContent()
val artists: String? = jsonElement["artists"].primitiveContent()
val authors: String? = jsonElement["authors"].primitiveContent()
val tags: String? =
jsonElement["tags"]?.jsonArray?.mapNotNull { it.primitiveContent() }?.joinToString()
require(title != null && slug != null)
return SManga.create().apply {
this.title = title
this.url = slug
this.thumbnail_url = thumbnail
this.status = status
this.description = summary
this.artist = artists
this.author = authors
this.genre = tags
}
}
}
object ListChapterDS : DeserializationStrategy<List<SChapter>> {
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
override fun deserialize(decoder: Decoder): List<SChapter> {
require(decoder is JsonDecoder)
val json = decoder.json
val jsonElement = decoder.decodeJsonElement()
require(jsonElement is JsonObject)
val jsonElementChapters = jsonElement["chapters"]?.jsonArray
require(jsonElementChapters != null)
return jsonElementChapters.map { chapter ->
json.decodeFromJsonElement(SChapterDS, chapter)
}.apply {
if (this.size == 1) this.first().chapter_number = 1F
}
}
}
private object SChapterDS : DeserializationStrategy<SChapter> {
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
override fun deserialize(decoder: Decoder): SChapter {
require(decoder is JsonDecoder)
val jsonElement = decoder.decodeJsonElement()
require(jsonElement is JsonObject)
val volume: Int? = jsonElement["volume"].primitiveInt()
val chapter: Float? = jsonElement["chapter"].primitiveFloat()
val title: String? = jsonElement["title"].primitiveContent()
val slug: String? = jsonElement["slug"].primitiveContent()
val releasedAt: String? = jsonElement["releasedAt"].primitiveContent()
require(slug != null && releasedAt != null)
return SChapter.create().apply {
if (chapter != null) this.chapter_number = chapter
this.name = chapterTitleBuilder(volume, title, chapter)
this.url = slug
this.date_upload = dateFormatter.parse(releasedAt)?.time ?: 0
}
}
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
/**
* Converts this Float into a String, removing any trailing .0
*/
private fun Float.toStringWithoutDotZero(): String = when (this % 1) {
0F -> this.toInt().toString()
else -> this.toString()
}
private fun chapterTitleBuilder(volume: Int?, title: String?, chapter: Float?): String {
val chapterTitle = StringBuilder()
if (volume != null) {
chapterTitle.append("Vol. $volume ")
}
if (chapter != null) {
chapterTitle.append("Chapter ${chapter.toStringWithoutDotZero()}")
}
if (title != null && title != "") {
if (chapterTitle.isNotEmpty()) chapterTitle.append(": ")
chapterTitle.append(title)
}
return chapterTitle.toString()
}
}
object ListPageDS : DeserializationStrategy<List<Page>> {
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
override fun deserialize(decoder: Decoder): List<Page> {
require(decoder is JsonDecoder)
val jsonElement = decoder.decodeJsonElement()
require(jsonElement is JsonObject)
val storage: String? = jsonElement["storage"].primitiveContent()
val manga: String? = jsonElement["manga"].primitiveContent()
val id: String? = jsonElement["id"].primitiveContent()
val images: List<String>? =
jsonElement["images"]?.jsonArray?.mapNotNull { it.primitiveContent() }
require(storage != null && manga != null && id != null && images != null)
val chapterUrl = "$storage/$manga/$id/"
return images.mapIndexed { index, pageSuffix -> Page(index, "", chapterUrl + pageSuffix) }
}
}