MangaMutiny - Migration from Gson to kotlinx.serialization (#7401)
* Initial serialization with kotlinx.serialization draft * Serialization without Serializable
This commit is contained in:
parent
897a5d94ba
commit
8dce249839
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue