From 9ccf78a721ff386bfd7343a70ee11da0b8d2b224 Mon Sep 17 00:00:00 2001 From: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> Date: Sat, 14 Jan 2023 11:46:28 -0300 Subject: [PATCH] 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. --- src/all/mangaplus/build.gradle | 2 +- .../extension/all/mangaplus/MangaPlus.kt | 95 +++++++++---------- .../extension/all/mangaplus/MangaPlusDto.kt | 13 ++- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/all/mangaplus/build.gradle b/src/all/mangaplus/build.gradle index fe483ba8f..8d5739334 100644 --- a/src/all/mangaplus/build.gradle +++ b/src/all/mangaplus/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'MANGA Plus by SHUEISHA' pkgNameSuffix = 'all.mangaplus' extClass = '.MangaPlusFactory' - extVersionCode = 39 + extVersionCode = 40 } apply from: "$rootDir/common.gradle" diff --git a/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlus.kt b/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlus.kt index cb453ead3..e40dd8912 100644 --- a/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlus.kt +++ b/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlus.kt @@ -19,7 +19,6 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -65,13 +64,12 @@ class MangaPlus( Injekt.get().getSharedPreferences("source_$id", 0x0000) } - private val imageQuality: String - get() = preferences.getString("${QUALITY_PREF_KEY}_$lang", QUALITY_PREF_DEFAULT_VALUE)!! - - private val splitImages: Boolean - get() = preferences.getBoolean("${SPLIT_PREF_KEY}_$lang", SPLIT_PREF_DEFAULT_VALUE) - - private var titleList: List? = null + /** + * 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 + * handling to avoid an additional request if possible. + */ + private var titleCache: Map<Int, Title>? = null override fun popularMangaRequest(page: Int): Request { val newHeaders = headersBuilder() @@ -88,12 +86,12 @@ class MangaPlus( result.error!!.langPopup(langCode)?.body ?: intl.unknownError } - titleList = result.success.titleRankingView!!.titles + val titleList = result.success.titleRankingView!!.titles .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 { @@ -116,8 +114,9 @@ class MangaPlus( .asMangaPlusResponse() if (popularResponse.success != null) { - titleList = popularResponse.success.titleRankingView!!.titles + titleCache = popularResponse.success.titleRankingView!!.titles .filter { it.language == langCode } + .associateBy(Title::titleId) } val mangas = result.success.webHomeViewV3!!.groups @@ -128,7 +127,7 @@ class MangaPlus( .map(Title::toSManga) .distinctBy(SManga::title) - return MangasPage(mangas, false) + return MangasPage(mangas, hasNextPage = false) } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { @@ -168,15 +167,9 @@ class MangaPlus( checkNotNull(result.success.mangaViewer.titleId) { intl.chapterExpired } val titleId = result.success.mangaViewer.titleId - val cacheTitle = titleList.orEmpty().firstOrNull { it.titleId == titleId } + val cachedTitle = titleCache?.get(titleId) - val manga = if (cacheTitle != null) { - SManga.create().apply { - title = result.success.mangaViewer.titleName!! - thumbnail_url = cacheTitle.portraitImageUrl - url = "#/titles/$titleId" - } - } else { + val title = cachedTitle?.toSManga() ?: run { val titleRequest = titleDetailsRequest(titleId.toString()) val titleResult = client.newCall(titleRequest).execute().asMangaPlusResponse() @@ -189,22 +182,23 @@ class MangaPlus( ?.toSManga() } - return MangasPage(listOfNotNull(manga), hasNextPage = false) + return MangasPage(listOfNotNull(title), hasNextPage = false) } val filter = response.request.url.queryParameter("filter").orEmpty() - titleList = result.success.allTitlesViewV2!!.allTitlesGroup + val allTitlesList = result.success.allTitlesViewV2!!.allTitlesGroup .flatMap(AllTitlesGroup::titles) .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 { @@ -271,8 +265,7 @@ class MangaPlus( val chapters = titleDetailView.firstChapterList + titleDetailView.lastChapterList return chapters.reversed() - // If the subTitle is null, then the chapter time expired. - .filter { it.subTitle != null } + .filterNot(Chapter::isExpired) .map(Chapter::toSChapter) } @@ -287,10 +280,10 @@ class MangaPlus( .set("Referer", "$baseUrl/viewer/$chapterId") .build() - val url = "$API_URL/manga_viewer".toHttpUrlOrNull()!!.newBuilder() + val url = "$API_URL/manga_viewer".toHttpUrl().newBuilder() .addQueryParameter("chapter_id", chapterId) - .addQueryParameter("split", if (splitImages) "yes" else "no") - .addQueryParameter("img_quality", imageQuality) + .addQueryParameter("split", if (preferences.splitImages) "yes" else "no") + .addQueryParameter("img_quality", preferences.imageQuality) .addQueryParameter("format", "json") .toString() @@ -363,7 +356,7 @@ class MangaPlus( 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 body = image.toResponseBody(contentType.toMediaTypeOrNull()) @@ -379,19 +372,19 @@ class MangaPlus( // Check if it is 404 to maintain compatibility when the extension used Weserv. val isBadCode = (response.code == 401 || response.code == 404) - if (isBadCode && request.url.toString().contains(TITLE_THUMBNAIL_PATH)) { - val titleId = request.url.toString() - .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) + if (!isBadCode && !request.url.toString().contains(TITLE_THUMBNAIL_PATH)) { + return response } - 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 { @@ -404,13 +397,19 @@ class MangaPlus( } 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 { 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) " + - "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 val QUALITY_PREF_ENTRY_VALUES = arrayOf("low", "high", "super_high") diff --git a/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlusDto.kt b/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlusDto.kt index 298d6848d..f144b00a1 100644 --- a/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlusDto.kt +++ b/src/all/mangaplus/src/eu/kanade/tachiyomi/extension/all/mangaplus/MangaPlusDto.kt @@ -83,6 +83,9 @@ data class TitleDetailView( private val isCompleted: Boolean get() = nonAppearanceInfo.contains(COMPLETED_REGEX) || isOneShot + private val isOnHiatus: Boolean + get() = nonAppearanceInfo.contains(HIATUS_REGEX) + private val genres: List<String> get() = listOfNotNull( "Simulrelease".takeIf { isSimulReleased && !isReEdition && !isOneShot }, @@ -93,12 +96,17 @@ data class TitleDetailView( fun toSManga(): SManga = title.toSManga().apply { 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() } companion object { 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() } } @@ -166,6 +174,9 @@ data class Chapter( val isVerticalOnly: Boolean = false ) { + val isExpired: Boolean + get() = subTitle == null + fun toSChapter(): SChapter = SChapter.create().apply { name = "${this@Chapter.name} - $subTitle" date_upload = 1000L * startTimeStamp