MangaPlus: improvements

pull/406/head
AwkwardPeak7 2 years ago
parent ab82d0eba0
commit 79fd2a58c1
No known key found for this signature in database

@ -1,25 +1,30 @@
package org.koitharu.kotatsu.parsers.site.all package org.koitharu.kotatsu.parsers.site.all
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource 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.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder 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.domain
import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull 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.json.toJSONList
import org.koitharu.kotatsu.parsers.util.parseJson import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import java.util.Locale import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.UUID 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 { class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.MANGAPLUSPARSER), Interceptor {
override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp") override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
@ -42,19 +47,11 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS
SortOrder.ALPHABETICAL 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()) private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
// no tags or tag search available
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) { if (offset > 0) {
return emptyList() return emptyList()
@ -63,55 +60,57 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS
return when (filter) { return when (filter) {
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> getPopularList(filter.locale) SortOrder.POPULARITY -> getPopularList()
SortOrder.UPDATED -> getLatestList(filter.locale) SortOrder.UPDATED -> getLatestList()
else -> getAllTitleList(filter.locale) else -> getAllTitleList()
} }
} }
is MangaListFilter.Search -> getAllTitleList(query = filter.query) is MangaListFilter.Search -> getAllTitleList(filter.query)
else -> getAllTitleList() else -> getAllTitleList()
} }
} }
private suspend fun getPopularList(locale: Locale?): List<Manga> { private suspend fun getPopularList(): List<Manga> {
val json = apiCall("/title_list/ranking") val json = apiCall("/title_list/ranking")
return json.getJSONObject("titleRankingView") return json.getJSONObject("titleRankingView")
.getJSONArray("titles") .getJSONArray("titles")
.toJSONList() .toJSONList()
.toMangaList(locale.toSiteLocale()) .toMangaList()
} }
private suspend fun getLatestList(locale: Locale?): List<Manga> { private suspend fun getLatestList(): List<Manga> {
val json = apiCall("/title_list/updated") val json = apiCall("/title_list/updated")
return json.getJSONObject("titleUpdatedView") return json.getJSONObject("titleUpdatedView")
.getJSONArray("latestTitle") .getJSONArray("latestTitle")
.mapJSON { it.getJSONObject("title") } .mapJSON { it.getJSONObject("title") }
.toMangaList(locale.toSiteLocale()) .toMangaList()
} }
private suspend fun getAllTitleList(locale: Locale? = null, query: String? = null): List<Manga> { // since search is local, save network calls on related manga call
val json = apiCall("/title_list/allV2") private val allTitleCache = SuspendLazy {
apiCall("/title_list/allV2")
return json.getJSONObject("allTitlesViewV2") .getJSONObject("allTitlesViewV2")
.getJSONArray("AllTitlesGroup") .getJSONArray("AllTitlesGroup")
.mapJSON { it.getJSONArray("titles").toJSONList() } .mapJSON { it.getJSONArray("titles").toJSONList() }
.flatten() .flatten()
.toMangaList(locale.toSiteLocale(), query) }
private suspend fun getAllTitleList(query: String? = null): List<Manga> {
return allTitleCache.get().toMangaList(query)
} }
private fun Collection<JSONObject>.toMangaList(langToFilter: String?, query: String? = null): List<Manga> { private fun List<JSONObject>.toMangaList(query: String? = null): List<Manga> {
return mapNotNull { return mapNotNull {
val language = it.getStringOrNull("language") ?: "ENGLISH" val language = it.getStringOrNull("language")
// filter out any other language other than langToFilter if (language != null)
if (langToFilter != null && language != langToFilter)
return@mapNotNull null return@mapNotNull null
val name = it.getString("name") 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 // filter out any other title or author which doesn't match search input
if (query != null && !(name.contains(query, true) || author.contains(query, true))) 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}") val json = apiCall("/title_detailV3?title_id=${manga.url}")
.getJSONObject("titleDetailView") .getJSONObject("titleDetailView")
val title = json.getJSONObject("title") val title = json.getJSONObject("title")
val chapterList = json.getJSONArray("chapterListGroup")
.toJSONList() val completed = json.getJSONObject("titleLabels")
.flatMap { .getString("releaseSchedule").let {
it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() + it == "DISABLED" || it == "COMPLETED"
it.optJSONArray("lastChapterList")?.toJSONList().orEmpty()
} }
val language = title.getStringOrNull("language") ?: "ENGLISH"
val hiatus = json.getStringOrNull("nonAppearanceInfo")?.contains("on a hiatus") == true
return manga.copy( return manga.copy(
title = title.getString("name"), title = title.getString("name"),
publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain), publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain),
coverUrl = title.getString("portraitImageUrl"), coverUrl = title.getString("portraitImageUrl"),
author = title.getString("author").replace(" / ", ", "), author = title.getString("author")
description = json.getString("overview"), .split("/").joinToString(transform = String::trim),
chapters = chapterList.mapNotNull { chapter -> description = buildString {
val chapterId = chapter.getInt("chapterId").toString() json.getString("overview").let(::append)
val name = chapter.getString("name") json.getStringOrNull("viewingPeriodDescription")
val subtitle = chapter.getStringOrNull("subTitle") ?.takeIf { !completed }
?: return@mapNotNull null ?.let { append("<br><br>", it) }
},
MangaChapter( chapters = getChapters(json),
id = generateUid(chapterId), state = if (completed) {
url = chapterId, MangaState.FINISHED
name = "$name - $subtitle", } else if (hiatus) {
number = name.substringAfter("#").toIntOrNull() ?: -1, MangaState.PAUSED
uploadDate = chapter.getInt("startTimeStamp") * 1000L, } else {
branch = language, MangaState.ONGOING
scanlator = null,
source = source
)
} }
) )
} }
private suspend fun getChapters(titleDetailView: JSONObject): List<MangaChapter> {
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<MangaChapter> {
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<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val pages = apiCall("/manga_viewer?chapter_id=${chapter.url}&split=yes&img_quality=super_high") val pages = apiCall("/manga_viewer?chapter_id=${chapter.url}&split=yes&img_quality=super_high")
.getJSONObject("mangaViewer") .getJSONObject("mangaViewer")
@ -193,6 +245,7 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS
} }
} }
// image descrambling
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
val response = chain.proceed(request) val response = chain.proceed(request)
@ -234,36 +287,15 @@ class MangaPlusParser(context: MangaLoaderContext) : MangaParser(context, MangaS
val reason = error.getJSONArray("popups") val reason = error.getJSONArray("popups")
.toJSONList() .toJSONList()
.firstOrNull { it.getStringOrNull("language") == null } .firstOrNull { it.getStringOrNull("language") == null }
?.getStringOrNull("body")
reason ?: "Unknown Error" if (reason?.getStringOrNull("subject") == "Not Found" && url.contains("manga_viewer")) {
} "This chapter has expired"
} } else {
reason?.getStringOrNull("body") ?: "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<MangaTag> {
return emptySet()
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
return emptyList()
}
companion object { companion object {
private const val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api" private const val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
} }

Loading…
Cancel
Save