From 8dce249839960ee512fe4929b6683bf62489203e Mon Sep 17 00:00:00 2001 From: E3FxGaming <8276268+E3FxGaming@users.noreply.github.com> Date: Thu, 3 Jun 2021 12:22:43 +0200 Subject: [PATCH] MangaMutiny - Migration from Gson to kotlinx.serialization (#7401) * Initial serialization with kotlinx.serialization draft * Serialization without Serializable --- src/en/mangamutiny/build.gradle | 3 +- .../extension/en/mangamutiny/MangaMutiny.kt | 179 +++-------------- .../mangamutiny/MangaMutinySerialization.kt | 183 ++++++++++++++++++ 3 files changed, 209 insertions(+), 156 deletions(-) create mode 100644 src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutinySerialization.kt diff --git a/src/en/mangamutiny/build.gradle b/src/en/mangamutiny/build.gradle index 94f3225f3..c29200ffb 100644 --- a/src/en/mangamutiny/build.gradle +++ b/src/en/mangamutiny/build.gradle @@ -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 } diff --git a/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutiny.kt b/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutiny.kt index 9660342f1..27ff5feed 100644 --- a/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutiny.kt +++ b/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutiny.kt @@ -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 { - val chapterList = mutableListOf() 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 { - val pageList = ArrayList() - 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() 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 { diff --git a/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutinySerialization.kt b/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutinySerialization.kt new file mode 100644 index 000000000..ad0262ccd --- /dev/null +++ b/src/en/mangamutiny/src/eu/kanade/tachiyomi/extension/en/mangamutiny/MangaMutinySerialization.kt @@ -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, Int>> { + override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor + + override fun deserialize(decoder: Decoder): Pair, 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 { + 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> { + override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor + + override fun deserialize(decoder: Decoder): List { + 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 { + + 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> { + override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor + + override fun deserialize(decoder: Decoder): List { + 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? = + 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) } + } +}