From ab82d0eba0ed66cf144cfd0214145fc664f974aa Mon Sep 17 00:00:00 2001 From: AwkwardPeak7 Date: Wed, 13 Dec 2023 21:18:57 +0500 Subject: [PATCH 1/3] MangaPlus --- .../parsers/site/all/MangaPlusParser.kt | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt new file mode 100644 index 000000000..9e710a3da --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt @@ -0,0 +1,271 @@ +package org.koitharu.kotatsu.parsers.site.all + +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.json.JSONObject +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaParser +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.generateUid +import org.koitharu.kotatsu.parsers.util.json.getStringOrNull +import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull +import org.koitharu.kotatsu.parsers.util.json.toJSONList +import org.koitharu.kotatsu.parsers.util.parseJson +import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl +import java.util.Locale +import java.util.UUID + +@MangaSourceParser("MANGAPLUSPARSER", "MangaPlus", type = ContentType.HENTAI) +class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.MANGAPLUSPARSER), Interceptor { + + override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp") + + override val availableSortOrders = setOf( + SortOrder.POPULARITY, + SortOrder.UPDATED, + SortOrder.ALPHABETICAL + ) + + override suspend fun getAvailableLocales()= setOf( + Locale.ENGLISH, + Locale("es"), + Locale.FRENCH, + Locale("id"), + Locale("pt_br"), + Locale("ru"), + Locale("th"), + Locale("vi") + ) + + private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString()) + + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { + if (offset > 0) { + return emptyList() + } + + return when (filter) { + is MangaListFilter.Advanced -> { + when (filter.sortOrder) { + SortOrder.POPULARITY -> getPopularList(filter.locale) + SortOrder.UPDATED -> getLatestList(filter.locale) + else -> getAllTitleList(filter.locale) + } + } + is MangaListFilter.Search -> getAllTitleList(query = filter.query) + else -> getAllTitleList() + } + } + + private suspend fun getPopularList(locale: Locale?): List { + val json = apiCall("/title_list/ranking") + + return json.getJSONObject("titleRankingView") + .getJSONArray("titles") + .toJSONList() + .toMangaList(locale.toSiteLocale()) + } + + private suspend fun getLatestList(locale: Locale?): List { + val json = apiCall("/title_list/updated") + + return json.getJSONObject("titleUpdatedView") + .getJSONArray("latestTitle") + .mapJSON { it.getJSONObject("title") } + .toMangaList(locale.toSiteLocale()) + } + + private suspend fun getAllTitleList(locale: Locale? = null, query: String? = null): List { + val json = apiCall("/title_list/allV2") + + return json.getJSONObject("allTitlesViewV2") + .getJSONArray("AllTitlesGroup") + .mapJSON { it.getJSONArray("titles").toJSONList() } + .flatten() + .toMangaList(locale.toSiteLocale(), query) + + } + + private fun Collection.toMangaList(langToFilter: String?, query: String? = null): List { + return mapNotNull { + val language = it.getStringOrNull("language") ?: "ENGLISH" + + // filter out any other language other than langToFilter + if (langToFilter != null && language != langToFilter) + return@mapNotNull null + + val name = it.getString("name") + val author = it.getString("author").replace(" / ", ", ") + + // filter out any other title or author which doesn't match search input + if (query != null && !(name.contains(query, true) || author.contains(query, true))) + return@mapNotNull null + + val titleId = it.getInt("titleId").toString() + + Manga( + id = generateUid(titleId), + url = titleId, + publicUrl = "/titles/$titleId".toAbsoluteUrl(domain), + title = name, + coverUrl = it.getString("portraitImageUrl"), + altTitle = null, + author = author, + isNsfw = false, + rating = RATING_UNKNOWN, + state = null, + source = source, + tags = emptySet() + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val json = apiCall("/title_detailV3?title_id=${manga.url}") + .getJSONObject("titleDetailView") + val title = json.getJSONObject("title") + val chapterList = json.getJSONArray("chapterListGroup") + .toJSONList() + .flatMap { + it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() + + it.optJSONArray("lastChapterList")?.toJSONList().orEmpty() + } + val language = title.getStringOrNull("language") ?: "ENGLISH" + + return manga.copy( + title = title.getString("name"), + publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain), + coverUrl = title.getString("portraitImageUrl"), + author = title.getString("author").replace(" / ", ", "), + description = json.getString("overview"), + chapters = chapterList.mapNotNull { chapter -> + val chapterId = chapter.getInt("chapterId").toString() + val name = chapter.getString("name") + val subtitle = chapter.getStringOrNull("subTitle") + ?: return@mapNotNull null + + MangaChapter( + id = generateUid(chapterId), + url = chapterId, + name = "$name - $subtitle", + number = name.substringAfter("#").toIntOrNull() ?: -1, + uploadDate = chapter.getInt("startTimeStamp") * 1000L, + branch = language, + scanlator = null, + source = source + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val pages = apiCall("/manga_viewer?chapter_id=${chapter.url}&split=yes&img_quality=super_high") + .getJSONObject("mangaViewer") + .getJSONArray("pages") + + return pages.mapJSONNotNull { + val mangaPage = it.optJSONObject("mangaPage") + ?: return@mapJSONNotNull null + val url = mangaPage.getString("imageUrl") + val encryptionKey = mangaPage.getStringOrNull("encryptionKey") + MangaPage( + id = generateUid(url), + url = url + if (encryptionKey == null ) "" else "#$encryptionKey", + preview = null, + source = source + ) + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + val encryptionKey = request.url.fragment + + if (encryptionKey.isNullOrEmpty()) { + return response + } + + val contentType = response.headers["Content-Type"] ?: "image/jpeg" + + val image = requireNotNull(response.body).bytes().decodeXorCipher(encryptionKey) + val body = image.toResponseBody(contentType.toMediaTypeOrNull()) + + return response.newBuilder() + .body(body) + .build() + } + + private fun ByteArray.decodeXorCipher(key: String): ByteArray { + val keyStream = key.chunked(2) + .map { it.toInt(16) } + + return mapIndexed { i, byte -> byte.toInt() xor keyStream[i % keyStream.size] } + .map(Int::toByte) + .toByteArray() + } + + private suspend fun apiCall(url: String): JSONObject { + val newUrl = "$apiUrl$url".toHttpUrl().newBuilder() + .addQueryParameter("format", "json") + .build() + val response = webClient.httpGet(newUrl, extraHeaders).parseJson() + + val success = response.optJSONObject("success") + + return checkNotNull(success) { + val error = response.getJSONObject("error") + val reason = error.getJSONArray("popups") + .toJSONList() + .firstOrNull { it.getStringOrNull("language") == null } + ?.getStringOrNull("body") + + reason ?: "Unknown Error" + } + } + + private fun Locale?.toSiteLocale(): String? { + if (this == null) return null + + return when { + equals(Locale.ENGLISH) -> "ENGLISH" + equals(Locale("es")) -> "SPANISH" + equals(Locale.FRENCH) -> "FRENCH" + equals(Locale("id")) -> "INDONESIAN" + equals(Locale("pt-BR")) -> "PORTUGUESE_BR" + equals(Locale("ru")) -> "RUSSIAN" + equals(Locale("th")) -> "THAI" + equals(Locale("vi")) -> "VIETNAMESE" + else -> null + } + } + + override suspend fun getAvailableTags(): Set { + return emptySet() + } + + override suspend fun getRelatedManga(seed: Manga): List { + return emptyList() + } + + companion object { + private const val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api" + } +} + From 79fd2a58c10cdb248f0a51a1f05ec466937b922d Mon Sep 17 00:00:00 2001 From: AwkwardPeak7 Date: Thu, 14 Dec 2023 19:19:32 +0500 Subject: [PATCH 2/3] MangaPlus: improvements --- .../parsers/site/all/MangaPlusParser.kt | 196 ++++++++++-------- 1 file changed, 114 insertions(+), 82 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt index 9e710a3da..350eb9c3b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt @@ -1,25 +1,30 @@ package org.koitharu.kotatsu.parsers.site.all +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody +import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.json.getStringOrNull @@ -28,10 +33,10 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull import org.koitharu.kotatsu.parsers.util.json.toJSONList import org.koitharu.kotatsu.parsers.util.parseJson import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl -import java.util.Locale +import org.koitharu.kotatsu.parsers.util.toTitleCase import java.util.UUID -@MangaSourceParser("MANGAPLUSPARSER", "MangaPlus", type = ContentType.HENTAI) +@MangaSourceParser("MANGAPLUSPARSER", "MANGA Plus by SHUEISHA") class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.MANGAPLUSPARSER), Interceptor { override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp") @@ -42,19 +47,11 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS SortOrder.ALPHABETICAL ) - override suspend fun getAvailableLocales()= setOf( - Locale.ENGLISH, - Locale("es"), - Locale.FRENCH, - Locale("id"), - Locale("pt_br"), - Locale("ru"), - Locale("th"), - Locale("vi") - ) - private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString()) + // no tags or tag search available + override suspend fun getAvailableTags(): Set = emptySet() + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { if (offset > 0) { return emptyList() @@ -63,55 +60,57 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS return when (filter) { is MangaListFilter.Advanced -> { when (filter.sortOrder) { - SortOrder.POPULARITY -> getPopularList(filter.locale) - SortOrder.UPDATED -> getLatestList(filter.locale) - else -> getAllTitleList(filter.locale) + SortOrder.POPULARITY -> getPopularList() + SortOrder.UPDATED -> getLatestList() + else -> getAllTitleList() } } - is MangaListFilter.Search -> getAllTitleList(query = filter.query) + is MangaListFilter.Search -> getAllTitleList(filter.query) else -> getAllTitleList() } } - private suspend fun getPopularList(locale: Locale?): List { + private suspend fun getPopularList(): List { val json = apiCall("/title_list/ranking") return json.getJSONObject("titleRankingView") .getJSONArray("titles") .toJSONList() - .toMangaList(locale.toSiteLocale()) + .toMangaList() } - private suspend fun getLatestList(locale: Locale?): List { + private suspend fun getLatestList(): List { val json = apiCall("/title_list/updated") return json.getJSONObject("titleUpdatedView") .getJSONArray("latestTitle") .mapJSON { it.getJSONObject("title") } - .toMangaList(locale.toSiteLocale()) + .toMangaList() } - private suspend fun getAllTitleList(locale: Locale? = null, query: String? = null): List { - val json = apiCall("/title_list/allV2") - - return json.getJSONObject("allTitlesViewV2") + // since search is local, save network calls on related manga call + private val allTitleCache = SuspendLazy { + apiCall("/title_list/allV2") + .getJSONObject("allTitlesViewV2") .getJSONArray("AllTitlesGroup") .mapJSON { it.getJSONArray("titles").toJSONList() } .flatten() - .toMangaList(locale.toSiteLocale(), query) + } + private suspend fun getAllTitleList(query: String? = null): List { + return allTitleCache.get().toMangaList(query) } - private fun Collection.toMangaList(langToFilter: String?, query: String? = null): List { + private fun List.toMangaList(query: String? = null): List { return mapNotNull { - val language = it.getStringOrNull("language") ?: "ENGLISH" + val language = it.getStringOrNull("language") - // filter out any other language other than langToFilter - if (langToFilter != null && language != langToFilter) + if (language != null) return@mapNotNull null val name = it.getString("name") - val author = it.getString("author").replace(" / ", ", ") + val author = it.getString("author") + .split("/").joinToString(transform = String::trim) // filter out any other title or author which doesn't match search input if (query != null && !(name.contains(query, true) || author.contains(query, true))) @@ -140,40 +139,93 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS val json = apiCall("/title_detailV3?title_id=${manga.url}") .getJSONObject("titleDetailView") val title = json.getJSONObject("title") - val chapterList = json.getJSONArray("chapterListGroup") - .toJSONList() - .flatMap { - it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() + - it.optJSONArray("lastChapterList")?.toJSONList().orEmpty() + + val completed = json.getJSONObject("titleLabels") + .getString("releaseSchedule").let { + it == "DISABLED" || it == "COMPLETED" } - val language = title.getStringOrNull("language") ?: "ENGLISH" + + val hiatus = json.getStringOrNull("nonAppearanceInfo")?.contains("on a hiatus") == true return manga.copy( title = title.getString("name"), publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain), coverUrl = title.getString("portraitImageUrl"), - author = title.getString("author").replace(" / ", ", "), - description = json.getString("overview"), - chapters = chapterList.mapNotNull { chapter -> - val chapterId = chapter.getInt("chapterId").toString() - val name = chapter.getString("name") - val subtitle = chapter.getStringOrNull("subTitle") - ?: return@mapNotNull null - - MangaChapter( - id = generateUid(chapterId), - url = chapterId, - name = "$name - $subtitle", - number = name.substringAfter("#").toIntOrNull() ?: -1, - uploadDate = chapter.getInt("startTimeStamp") * 1000L, - branch = language, - scanlator = null, - source = source - ) + author = title.getString("author") + .split("/").joinToString(transform = String::trim), + description = buildString { + json.getString("overview").let(::append) + json.getStringOrNull("viewingPeriodDescription") + ?.takeIf { !completed } + ?.let { append("

", it) } + }, + chapters = getChapters(json), + state = if (completed) { + MangaState.FINISHED + } else if (hiatus) { + MangaState.PAUSED + } else { + MangaState.ONGOING } ) } + private suspend fun getChapters(titleDetailView: JSONObject): List { + val currentLang = titleDetailView.getJSONObject("title").getStringOrNull("language") ?: "ENGLISH" + val chapters = parseChapters( + titleDetailView.getJSONArray("chapterListGroup"), + currentLang + ) + + return chapters + coroutineScope { + titleDetailView.getJSONArray("titleLanguages") + .toJSONList() + .filterNot { (it.getStringOrNull("language") ?: "ENGLISH") == currentLang } + .map { + async { + val titleId = it.getInt("titleId").toString() + val language = it.getStringOrNull("language") ?: "ENGLISH" + val chapGroup = apiCall("/title_detailV3?title_id=$titleId") + .getJSONObject("titleDetailView") + .getJSONArray("chapterListGroup") + + parseChapters(chapGroup, language) + } + } + }.awaitAll().flatten() + } + + private fun parseChapters(chapterListGroup: JSONArray, language: String): List { + val chapterList = chapterListGroup + .toJSONList() + .flatMap { + it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() + + it.optJSONArray("lastChapterList")?.toJSONList().orEmpty() + } + + return chapterList.mapNotNull { chapter -> + val chapterId = chapter.getInt("chapterId").toString() + val subtitle = chapter.getStringOrNull("subTitle") + ?: return@mapNotNull null + + MangaChapter( + id = generateUid(chapterId), + url = chapterId, + name = subtitle, + number = chapter.getString("name") + .substringAfter("#") + .toIntOrNull() ?: -1, + uploadDate = chapter.getInt("startTimeStamp") * 1000L, + branch = when (language) { + "PORTUGUESE_BR" -> "Portuguese (Brazil)" + else -> language.lowercase().toTitleCase() + }, + scanlator = null, + source = source + ) + } + } + override suspend fun getPages(chapter: MangaChapter): List { val pages = apiCall("/manga_viewer?chapter_id=${chapter.url}&split=yes&img_quality=super_high") .getJSONObject("mangaViewer") @@ -193,6 +245,7 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS } } + // image descrambling override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) @@ -234,36 +287,15 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS val reason = error.getJSONArray("popups") .toJSONList() .firstOrNull { it.getStringOrNull("language") == null } - ?.getStringOrNull("body") - reason ?: "Unknown Error" - } - } - - private fun Locale?.toSiteLocale(): String? { - if (this == null) return null - - return when { - equals(Locale.ENGLISH) -> "ENGLISH" - equals(Locale("es")) -> "SPANISH" - equals(Locale.FRENCH) -> "FRENCH" - equals(Locale("id")) -> "INDONESIAN" - equals(Locale("pt-BR")) -> "PORTUGUESE_BR" - equals(Locale("ru")) -> "RUSSIAN" - equals(Locale("th")) -> "THAI" - equals(Locale("vi")) -> "VIETNAMESE" - else -> null + if (reason?.getStringOrNull("subject") == "Not Found" && url.contains("manga_viewer")) { + "This chapter has expired" + } else { + reason?.getStringOrNull("body") ?: "Unknown Error" + } } } - override suspend fun getAvailableTags(): Set { - return emptySet() - } - - override suspend fun getRelatedManga(seed: Manga): List { - return emptyList() - } - companion object { private const val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api" } From 3034a87929bd2b245dc9bc7e8acf22711fc81156 Mon Sep 17 00:00:00 2001 From: AwkwardPeak7 Date: Fri, 15 Dec 2023 18:22:43 +0500 Subject: [PATCH 3/3] MangaPlus: divide different languages to avoid too many calls during chapter fetching & prevent some non-english titles to be filtered out --- .../parsers/site/all/MangaPlusParser.kt | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt index 350eb9c3b..dcde9fc51 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaPlusParser.kt @@ -1,8 +1,5 @@ package org.koitharu.kotatsu.parsers.site.all -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor @@ -36,8 +33,11 @@ import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl import org.koitharu.kotatsu.parsers.util.toTitleCase import java.util.UUID -@MangaSourceParser("MANGAPLUSPARSER", "MANGA Plus by SHUEISHA") -class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.MANGAPLUSPARSER), Interceptor { +internal abstract class MangaPlusParser( + context: MangaLoaderContext, + source: MangaSource, + private val sourceLang: String +) : MangaParser(context, source), Interceptor { override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp") @@ -103,9 +103,9 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS private fun List.toMangaList(query: String? = null): List { return mapNotNull { - val language = it.getStringOrNull("language") + val language = it.getStringOrNull("language") ?: "ENGLISH" - if (language != null) + if (language != sourceLang) return@mapNotNull null val name = it.getString("name") @@ -159,7 +159,10 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS ?.takeIf { !completed } ?.let { append("

", it) } }, - chapters = getChapters(json), + chapters = parseChapters( + json.getJSONArray("chapterListGroup"), + title.getStringOrNull("language") ?: "ENGLISH" + ), state = if (completed) { MangaState.FINISHED } else if (hiatus) { @@ -170,31 +173,6 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS ) } - private suspend fun getChapters(titleDetailView: JSONObject): List { - val currentLang = titleDetailView.getJSONObject("title").getStringOrNull("language") ?: "ENGLISH" - val chapters = parseChapters( - titleDetailView.getJSONArray("chapterListGroup"), - currentLang - ) - - return chapters + coroutineScope { - titleDetailView.getJSONArray("titleLanguages") - .toJSONList() - .filterNot { (it.getStringOrNull("language") ?: "ENGLISH") == currentLang } - .map { - async { - val titleId = it.getInt("titleId").toString() - val language = it.getStringOrNull("language") ?: "ENGLISH" - val chapGroup = apiCall("/title_detailV3?title_id=$titleId") - .getJSONObject("titleDetailView") - .getJSONArray("chapterListGroup") - - parseChapters(chapGroup, language) - } - } - }.awaitAll().flatten() - } - private fun parseChapters(chapterListGroup: JSONArray, language: String): List { val chapterList = chapterListGroup .toJSONList() @@ -299,5 +277,60 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS companion object { private const val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api" } -} + @MangaSourceParser("MANGAPLUSPARSER_EN", "MANGA Plus English", "en") + class English(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_EN, + "ENGLISH" + ) + + @MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus Spanish", "es") + class Spanish(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_ES, + "SPANISH" + ) + + @MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus French", "fr") + class French(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_FR, + "FRENCH" + ) + + @MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus Indonesian", "id") + class Indonesian(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_ID, + "INDONESIAN" + ) + + @MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt") + class Portuguese(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_PTBR, + "PORTUGUESE_BR" + ) + + @MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus Russian", "ru") + class Russian(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_RU, + "RUSSIAN" + ) + + @MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus Thai", "th") + class Thai(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_TH, + "THAI" + ) + + @MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus Vietnamese", "vi") + class Vietnamese(context: MangaLoaderContext) : MangaPlusParser( + context, + MangaSource.MANGAPLUSPARSER_VI, + "VIETNAMESE" + ) +}