Add support to "On hiatus" status on MangaPlus (#14941)
* Refactor the MangaPlus code a bit. * Also handle the newer on hiatus status. * Change the title cache to a map to better performance.
This commit is contained in:
parent
a258a213c2
commit
9ccf78a721
@ -6,7 +6,7 @@ ext {
|
|||||||
extName = 'MANGA Plus by SHUEISHA'
|
extName = 'MANGA Plus by SHUEISHA'
|
||||||
pkgNameSuffix = 'all.mangaplus'
|
pkgNameSuffix = 'all.mangaplus'
|
||||||
extClass = '.MangaPlusFactory'
|
extClass = '.MangaPlusFactory'
|
||||||
extVersionCode = 39
|
extVersionCode = 40
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -19,7 +19,6 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -65,13 +64,12 @@ class MangaPlus(
|
|||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val imageQuality: String
|
/**
|
||||||
get() = preferences.getString("${QUALITY_PREF_KEY}_$lang", QUALITY_PREF_DEFAULT_VALUE)!!
|
* Private cache to find the newest thumbnail URL in case the existing one
|
||||||
|
* in Tachiyomi database is expired. It's also used during the chapter deeplink
|
||||||
private val splitImages: Boolean
|
* handling to avoid an additional request if possible.
|
||||||
get() = preferences.getBoolean("${SPLIT_PREF_KEY}_$lang", SPLIT_PREF_DEFAULT_VALUE)
|
*/
|
||||||
|
private var titleCache: Map<Int, Title>? = null
|
||||||
private var titleList: List<Title>? = null
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val newHeaders = headersBuilder()
|
val newHeaders = headersBuilder()
|
||||||
@ -88,12 +86,12 @@ class MangaPlus(
|
|||||||
result.error!!.langPopup(langCode)?.body ?: intl.unknownError
|
result.error!!.langPopup(langCode)?.body ?: intl.unknownError
|
||||||
}
|
}
|
||||||
|
|
||||||
titleList = result.success.titleRankingView!!.titles
|
val titleList = result.success.titleRankingView!!.titles
|
||||||
.filter { it.language == langCode }
|
.filter { it.language == langCode }
|
||||||
|
|
||||||
val mangas = titleList!!.map(Title::toSManga)
|
titleCache = titleList.associateBy(Title::titleId)
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(titleList.map(Title::toSManga), hasNextPage = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
@ -116,8 +114,9 @@ class MangaPlus(
|
|||||||
.asMangaPlusResponse()
|
.asMangaPlusResponse()
|
||||||
|
|
||||||
if (popularResponse.success != null) {
|
if (popularResponse.success != null) {
|
||||||
titleList = popularResponse.success.titleRankingView!!.titles
|
titleCache = popularResponse.success.titleRankingView!!.titles
|
||||||
.filter { it.language == langCode }
|
.filter { it.language == langCode }
|
||||||
|
.associateBy(Title::titleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangas = result.success.webHomeViewV3!!.groups
|
val mangas = result.success.webHomeViewV3!!.groups
|
||||||
@ -128,7 +127,7 @@ class MangaPlus(
|
|||||||
.map(Title::toSManga)
|
.map(Title::toSManga)
|
||||||
.distinctBy(SManga::title)
|
.distinctBy(SManga::title)
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, hasNextPage = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
@ -168,15 +167,9 @@ class MangaPlus(
|
|||||||
checkNotNull(result.success.mangaViewer.titleId) { intl.chapterExpired }
|
checkNotNull(result.success.mangaViewer.titleId) { intl.chapterExpired }
|
||||||
|
|
||||||
val titleId = result.success.mangaViewer.titleId
|
val titleId = result.success.mangaViewer.titleId
|
||||||
val cacheTitle = titleList.orEmpty().firstOrNull { it.titleId == titleId }
|
val cachedTitle = titleCache?.get(titleId)
|
||||||
|
|
||||||
val manga = if (cacheTitle != null) {
|
val title = cachedTitle?.toSManga() ?: run {
|
||||||
SManga.create().apply {
|
|
||||||
title = result.success.mangaViewer.titleName!!
|
|
||||||
thumbnail_url = cacheTitle.portraitImageUrl
|
|
||||||
url = "#/titles/$titleId"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val titleRequest = titleDetailsRequest(titleId.toString())
|
val titleRequest = titleDetailsRequest(titleId.toString())
|
||||||
val titleResult = client.newCall(titleRequest).execute().asMangaPlusResponse()
|
val titleResult = client.newCall(titleRequest).execute().asMangaPlusResponse()
|
||||||
|
|
||||||
@ -189,22 +182,23 @@ class MangaPlus(
|
|||||||
?.toSManga()
|
?.toSManga()
|
||||||
}
|
}
|
||||||
|
|
||||||
return MangasPage(listOfNotNull(manga), hasNextPage = false)
|
return MangasPage(listOfNotNull(title), hasNextPage = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val filter = response.request.url.queryParameter("filter").orEmpty()
|
val filter = response.request.url.queryParameter("filter").orEmpty()
|
||||||
|
|
||||||
titleList = result.success.allTitlesViewV2!!.allTitlesGroup
|
val allTitlesList = result.success.allTitlesViewV2!!.allTitlesGroup
|
||||||
.flatMap(AllTitlesGroup::titles)
|
.flatMap(AllTitlesGroup::titles)
|
||||||
.filter { it.language == langCode }
|
.filter { it.language == langCode }
|
||||||
.filter { title ->
|
|
||||||
title.name.contains(filter, ignoreCase = true) ||
|
|
||||||
title.author.orEmpty().contains(filter, ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangas = titleList!!.map(Title::toSManga)
|
titleCache = allTitlesList.associateBy(Title::titleId)
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage = false)
|
val searchResults = allTitlesList.filter { title ->
|
||||||
|
title.name.contains(filter, ignoreCase = true) ||
|
||||||
|
title.author.orEmpty().contains(filter, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(searchResults.map(Title::toSManga), hasNextPage = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun titleDetailsRequest(mangaUrl: String): Request {
|
private fun titleDetailsRequest(mangaUrl: String): Request {
|
||||||
@ -271,8 +265,7 @@ class MangaPlus(
|
|||||||
val chapters = titleDetailView.firstChapterList + titleDetailView.lastChapterList
|
val chapters = titleDetailView.firstChapterList + titleDetailView.lastChapterList
|
||||||
|
|
||||||
return chapters.reversed()
|
return chapters.reversed()
|
||||||
// If the subTitle is null, then the chapter time expired.
|
.filterNot(Chapter::isExpired)
|
||||||
.filter { it.subTitle != null }
|
|
||||||
.map(Chapter::toSChapter)
|
.map(Chapter::toSChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,10 +280,10 @@ class MangaPlus(
|
|||||||
.set("Referer", "$baseUrl/viewer/$chapterId")
|
.set("Referer", "$baseUrl/viewer/$chapterId")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val url = "$API_URL/manga_viewer".toHttpUrlOrNull()!!.newBuilder()
|
val url = "$API_URL/manga_viewer".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("chapter_id", chapterId)
|
.addQueryParameter("chapter_id", chapterId)
|
||||||
.addQueryParameter("split", if (splitImages) "yes" else "no")
|
.addQueryParameter("split", if (preferences.splitImages) "yes" else "no")
|
||||||
.addQueryParameter("img_quality", imageQuality)
|
.addQueryParameter("img_quality", preferences.imageQuality)
|
||||||
.addQueryParameter("format", "json")
|
.addQueryParameter("format", "json")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
@ -363,7 +356,7 @@ class MangaPlus(
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentType = response.header("Content-Type", "image/jpeg")!!
|
val contentType = response.headers["Content-Type"] ?: "image/jpeg"
|
||||||
val image = response.body!!.bytes().decodeXorCipher(encryptionKey)
|
val image = response.body!!.bytes().decodeXorCipher(encryptionKey)
|
||||||
val body = image.toResponseBody(contentType.toMediaTypeOrNull())
|
val body = image.toResponseBody(contentType.toMediaTypeOrNull())
|
||||||
|
|
||||||
@ -379,19 +372,19 @@ class MangaPlus(
|
|||||||
// Check if it is 404 to maintain compatibility when the extension used Weserv.
|
// Check if it is 404 to maintain compatibility when the extension used Weserv.
|
||||||
val isBadCode = (response.code == 401 || response.code == 404)
|
val isBadCode = (response.code == 401 || response.code == 404)
|
||||||
|
|
||||||
if (isBadCode && request.url.toString().contains(TITLE_THUMBNAIL_PATH)) {
|
if (!isBadCode && !request.url.toString().contains(TITLE_THUMBNAIL_PATH)) {
|
||||||
val titleId = request.url.toString()
|
return response
|
||||||
.substringBefore("/$TITLE_THUMBNAIL_PATH")
|
|
||||||
.substringAfterLast("/")
|
|
||||||
.toInt()
|
|
||||||
val title = titleList?.find { it.titleId == titleId } ?: return response
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
val thumbnailRequest = GET(title.portraitImageUrl, request.headers)
|
|
||||||
return chain.proceed(thumbnailRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
val titleId = request.url.toString()
|
||||||
|
.substringBefore("/$TITLE_THUMBNAIL_PATH")
|
||||||
|
.substringAfterLast("/")
|
||||||
|
.toInt()
|
||||||
|
val title = titleCache?.get(titleId) ?: return response
|
||||||
|
|
||||||
|
response.close()
|
||||||
|
val thumbnailRequest = GET(title.portraitImageUrl, request.headers)
|
||||||
|
return chain.proceed(thumbnailRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ByteArray.decodeXorCipher(key: String): ByteArray {
|
private fun ByteArray.decodeXorCipher(key: String): ByteArray {
|
||||||
@ -404,13 +397,19 @@ class MangaPlus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Response.asMangaPlusResponse(): MangaPlusResponse = use {
|
private fun Response.asMangaPlusResponse(): MangaPlusResponse = use {
|
||||||
json.decodeFromString(body!!.string())
|
json.decodeFromString(body?.string().orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val SharedPreferences.imageQuality: String
|
||||||
|
get() = getString("${QUALITY_PREF_KEY}_$lang", QUALITY_PREF_DEFAULT_VALUE)!!
|
||||||
|
|
||||||
|
private val SharedPreferences.splitImages: Boolean
|
||||||
|
get() = getBoolean("${SPLIT_PREF_KEY}_$lang", SPLIT_PREF_DEFAULT_VALUE)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val API_URL = "https://jumpg-webapi.tokyo-cdn.com/api"
|
private const val API_URL = "https://jumpg-webapi.tokyo-cdn.com/api"
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
private const val QUALITY_PREF_KEY = "imageResolution"
|
private const val QUALITY_PREF_KEY = "imageResolution"
|
||||||
private val QUALITY_PREF_ENTRY_VALUES = arrayOf("low", "high", "super_high")
|
private val QUALITY_PREF_ENTRY_VALUES = arrayOf("low", "high", "super_high")
|
||||||
|
@ -83,6 +83,9 @@ data class TitleDetailView(
|
|||||||
private val isCompleted: Boolean
|
private val isCompleted: Boolean
|
||||||
get() = nonAppearanceInfo.contains(COMPLETED_REGEX) || isOneShot
|
get() = nonAppearanceInfo.contains(COMPLETED_REGEX) || isOneShot
|
||||||
|
|
||||||
|
private val isOnHiatus: Boolean
|
||||||
|
get() = nonAppearanceInfo.contains(HIATUS_REGEX)
|
||||||
|
|
||||||
private val genres: List<String>
|
private val genres: List<String>
|
||||||
get() = listOfNotNull(
|
get() = listOfNotNull(
|
||||||
"Simulrelease".takeIf { isSimulReleased && !isReEdition && !isOneShot },
|
"Simulrelease".takeIf { isSimulReleased && !isReEdition && !isOneShot },
|
||||||
@ -93,12 +96,17 @@ data class TitleDetailView(
|
|||||||
|
|
||||||
fun toSManga(): SManga = title.toSManga().apply {
|
fun toSManga(): SManga = title.toSManga().apply {
|
||||||
description = (overview.orEmpty() + "\n\n" + viewingPeriodDescription).trim()
|
description = (overview.orEmpty() + "\n\n" + viewingPeriodDescription).trim()
|
||||||
status = if (isCompleted) SManga.COMPLETED else SManga.ONGOING
|
status = when {
|
||||||
|
isCompleted -> SManga.COMPLETED
|
||||||
|
isOnHiatus -> SManga.ON_HIATUS
|
||||||
|
else -> SManga.ONGOING
|
||||||
|
}
|
||||||
genre = genres.joinToString()
|
genre = genres.joinToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val COMPLETED_REGEX = "completado|complete|completo".toRegex()
|
private val COMPLETED_REGEX = "completado|complete|completo".toRegex()
|
||||||
|
private val HIATUS_REGEX = "on a hiatus".toRegex(RegexOption.IGNORE_CASE)
|
||||||
private val REEDITION_REGEX = "revival|remasterizada".toRegex()
|
private val REEDITION_REGEX = "revival|remasterizada".toRegex()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,6 +174,9 @@ data class Chapter(
|
|||||||
val isVerticalOnly: Boolean = false
|
val isVerticalOnly: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val isExpired: Boolean
|
||||||
|
get() = subTitle == null
|
||||||
|
|
||||||
fun toSChapter(): SChapter = SChapter.create().apply {
|
fun toSChapter(): SChapter = SChapter.create().apply {
|
||||||
name = "${this@Chapter.name} - $subTitle"
|
name = "${this@Chapter.name} - $subTitle"
|
||||||
date_upload = 1000L * startTimeStamp
|
date_upload = 1000L * startTimeStamp
|
||||||
|
Loading…
x
Reference in New Issue
Block a user