Refactor the MangaPlus code. (#10225)

This commit is contained in:
Alessandro Jean 2021-12-27 08:06:14 -03:00 committed by GitHub
parent 63e9f627e0
commit 1175d23ac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 63 deletions

View File

@ -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 = 26 extVersionCode = 27
} }
dependencies { dependencies {

View File

@ -5,7 +5,7 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -29,7 +30,6 @@ import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit
abstract class MangaPlus( abstract class MangaPlus(
override val lang: String, override val lang: String,
@ -37,11 +37,11 @@ abstract class MangaPlus(
private val langCode: Language private val langCode: Language
) : HttpSource(), ConfigurableSource { ) : HttpSource(), ConfigurableSource {
override val name = "MANGA Plus by SHUEISHA" final override val name = "MANGA Plus by SHUEISHA"
override val baseUrl = "https://mangaplus.shueisha.co.jp" final override val baseUrl = "https://mangaplus.shueisha.co.jp"
override val supportsLatest = true final override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl) .add("Origin", baseUrl)
@ -52,18 +52,19 @@ abstract class MangaPlus(
override val client: OkHttpClient = network.client.newBuilder() override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(::imageIntercept) .addInterceptor(::imageIntercept)
.addInterceptor(::thumbnailIntercept) .addInterceptor(::thumbnailIntercept)
.addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS)) .addInterceptor(SpecificHostRateLimitInterceptor(API_URL.toHttpUrl(), 1))
.addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 2))
.build() .build()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val imageResolution: String private val imageQuality: String
get() = preferences.getString("${RESOLUTION_PREF_KEY}_$lang", RESOLUTION_PREF_DEFAULT_VALUE)!! get() = preferences.getString("${QUALITY_PREF_KEY}_$lang", QUALITY_PREF_DEFAULT_VALUE)!!
private val splitImages: String private val splitImages: Boolean
get() = if (preferences.getBoolean("${SPLIT_PREF_KEY}_$lang", SPLIT_PREF_DEFAULT_VALUE)) "yes" else "no" get() = preferences.getBoolean("${SPLIT_PREF_KEY}_$lang", SPLIT_PREF_DEFAULT_VALUE)
private var titleList: List<Title>? = null private var titleList: List<Title>? = null
@ -118,9 +119,9 @@ abstract class MangaPlus(
} }
val mangas = result.success.webHomeViewV3!!.groups val mangas = result.success.webHomeViewV3!!.groups
.flatMap { it.titleGroups } .flatMap(UpdatedTitleV2Group::titleGroups)
.flatMap { it.titles } .flatMap(OriginalTitleGroup::titles)
.map { it.title } .map(UpdatedTitle::title)
.filter { it.language == langCode } .filter { it.language == langCode }
.map { .map {
SManga.create().apply { SManga.create().apply {
@ -129,7 +130,7 @@ abstract class MangaPlus(
url = "#/titles/${it.titleId}" url = "#/titles/${it.titleId}"
} }
} }
.distinctBy { it.title } .distinctBy(SManga::title)
return MangasPage(mangas, false) return MangasPage(mangas, false)
} }
@ -141,7 +142,10 @@ abstract class MangaPlus(
return@map it return@map it
} }
val filteredResult = it.mangas.filter { m -> m.title.contains(query, true) } val filteredResult = it.mangas.filter { manga ->
manga.title.contains(query, true)
}
MangasPage(filteredResult, it.hasNextPage) MangasPage(filteredResult, it.hasNextPage)
} }
} }
@ -181,7 +185,7 @@ abstract class MangaPlus(
} }
titleList = result.success.allTitlesViewV2!!.allTitlesGroup titleList = result.success.allTitlesViewV2!!.allTitlesGroup
.flatMap { it.titles } .flatMap(AllTitlesGroup::titles)
.filter { it.language == langCode } .filter { it.language == langCode }
val mangas = titleList!!.map { val mangas = titleList!!.map {
@ -227,13 +231,13 @@ abstract class MangaPlus(
val details = result.success.titleDetailView!! val details = result.success.titleDetailView!!
val title = details.title val title = details.title
val isCompleted = details.nonAppearanceInfo.contains(COMPLETE_REGEX)
return SManga.create().apply { return SManga.create().apply {
author = title.author.replace(" / ", ", ") author = title.author.replace(" / ", ", ")
artist = author artist = author
description = details.overview + "\n\n" + details.viewingPeriodDescription description = details.overview + "\n\n" + details.viewingPeriodDescription
status = if (isCompleted) SManga.COMPLETED else SManga.ONGOING status = if (details.isCompleted) SManga.COMPLETED else SManga.ONGOING
genre = details.genres.filter(String::isNotEmpty).joinToString()
thumbnail_url = title.portraitImageUrl thumbnail_url = title.portraitImageUrl
} }
} }
@ -256,7 +260,6 @@ abstract class MangaPlus(
.map { .map {
SChapter.create().apply { SChapter.create().apply {
name = "${it.name} - ${it.subTitle}" name = "${it.name} - ${it.subTitle}"
scanlator = "Shueisha"
date_upload = 1000L * it.startTimeStamp date_upload = 1000L * it.startTimeStamp
url = "#/viewer/${it.chapterId}" url = "#/viewer/${it.chapterId}"
chapter_number = it.name.substringAfter("#").toFloatOrNull() ?: -1f chapter_number = it.name.substringAfter("#").toFloatOrNull() ?: -1f
@ -273,8 +276,8 @@ abstract class MangaPlus(
val url = "$API_URL/manga_viewer".toHttpUrlOrNull()!!.newBuilder() val url = "$API_URL/manga_viewer".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("chapter_id", chapterId) .addQueryParameter("chapter_id", chapterId)
.addQueryParameter("split", splitImages) .addQueryParameter("split", if (splitImages) "yes" else "no")
.addQueryParameter("img_quality", imageResolution) .addQueryParameter("img_quality", imageQuality)
.toString() .toString()
return GET(url, newHeaders) return GET(url, newHeaders)
@ -289,10 +292,11 @@ abstract class MangaPlus(
val referer = response.request.header("Referer")!! val referer = response.request.header("Referer")!!
return result.success.mangaViewer!!.pages return result.success.mangaViewer!!.pages
.mapNotNull { it.page } .mapNotNull(MangaPlusPage::page)
.mapIndexed { i, page -> .mapIndexed { i, page ->
val encryptionKey = if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}" val encryptionKey = if (page.encryptionKey == null) "" else
Page(i, referer, "${page.imageUrl}$encryptionKey") "&encryptionKey=${page.encryptionKey}"
Page(i, referer, page.imageUrl + encryptionKey)
} }
} }
@ -310,12 +314,12 @@ abstract class MangaPlus(
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val resolutionPref = ListPreference(screen.context).apply { val qualityPref = ListPreference(screen.context).apply {
key = "${RESOLUTION_PREF_KEY}_$lang" key = "${QUALITY_PREF_KEY}_$lang"
title = RESOLUTION_PREF_TITLE title = QUALITY_PREF_TITLE
entries = RESOLUTION_PREF_ENTRIES entries = QUALITY_PREF_ENTRIES
entryValues = RESOLUTION_PREF_ENTRY_VALUES entryValues = QUALITY_PREF_ENTRY_VALUES
setDefaultValue(RESOLUTION_PREF_DEFAULT_VALUE) setDefaultValue(QUALITY_PREF_DEFAULT_VALUE)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -324,7 +328,7 @@ abstract class MangaPlus(
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit() preferences.edit()
.putString("${RESOLUTION_PREF_KEY}_$lang", entry) .putString("${QUALITY_PREF_KEY}_$lang", entry)
.commit() .commit()
} }
} }
@ -344,7 +348,7 @@ abstract class MangaPlus(
} }
} }
screen.addPreference(resolutionPref) screen.addPreference(qualityPref)
screen.addPreference(splitPref) screen.addPreference(splitPref)
} }
@ -375,23 +379,15 @@ abstract class MangaPlus(
.build() .build()
} }
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray { private fun decodeImage(encryptionKey: String, imageBytes: ByteArray): ByteArray {
val keyStream = HEX_GROUP val keyStream = encryptionKey
.findAll(encryptionKey) .chunked(2)
.toList() .map { it.toInt(16) }
.map { it.groupValues[1].toInt(16) }
val content = image return imageBytes
.map { it.toInt() } .mapIndexed { i, byte -> byte.toInt() xor keyStream[i % keyStream.size] }
.toMutableList() .map(Int::toByte)
.toByteArray()
val blockSizeInBytes = keyStream.size
for ((i, value) in content.iterator().withIndex()) {
content[i] = value xor keyStream[i % blockSizeInBytes]
}
return ByteArray(content.size) { pos -> content[pos].toByte() }
} }
private fun thumbnailIntercept(chain: Interceptor.Chain): Response { private fun thumbnailIntercept(chain: Interceptor.Chain): Response {
@ -422,29 +418,28 @@ abstract class MangaPlus(
else -> englishPopup else -> englishPopup
} }
private fun Response.asProto(): MangaPlusResponse { private fun Response.asProto(): MangaPlusResponse = use {
return ProtoBuf.decodeFromByteArray(body!!.bytes()) ProtoBuf.decodeFromByteArray(body!!.bytes())
} }
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/96.0.4664.45 Safari/537.36" "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.3"
private val HEX_GROUP = "(.{1,2})".toRegex() private const val QUALITY_PREF_KEY = "imageResolution"
private const val QUALITY_PREF_TITLE = "Image quality"
private const val RESOLUTION_PREF_KEY = "imageResolution" private val QUALITY_PREF_ENTRIES = arrayOf("Low", "Medium", "High")
private const val RESOLUTION_PREF_TITLE = "Image resolution" private val QUALITY_PREF_ENTRY_VALUES = arrayOf("low", "high", "super_high")
private val RESOLUTION_PREF_ENTRIES = arrayOf("Low resolution", "Medium resolution", "High resolution") private val QUALITY_PREF_DEFAULT_VALUE = QUALITY_PREF_ENTRY_VALUES[2]
private val RESOLUTION_PREF_ENTRY_VALUES = arrayOf("low", "high", "super_high")
private val RESOLUTION_PREF_DEFAULT_VALUE = RESOLUTION_PREF_ENTRY_VALUES[2]
private const val SPLIT_PREF_KEY = "splitImage" private const val SPLIT_PREF_KEY = "splitImage"
private const val SPLIT_PREF_TITLE = "Split double pages" private const val SPLIT_PREF_TITLE = "Split double pages"
private const val SPLIT_PREF_SUMMARY = "Only a few titles supports disabling this setting." private const val SPLIT_PREF_SUMMARY = "Only a few titles supports disabling this setting."
private const val SPLIT_PREF_DEFAULT_VALUE = true private const val SPLIT_PREF_DEFAULT_VALUE = true
private val COMPLETE_REGEX = "completado|complete".toRegex() val COMPLETED_REGEX = "completado|complete|completo".toRegex()
val REEDITION_REGEX = "revival|remasterizada".toRegex()
private const val TITLE_THUMBNAIL_PATH = "title_thumbnail_portrait_list" private const val TITLE_THUMBNAIL_PATH = "title_thumbnail_portrait_list"

View File

@ -59,9 +59,34 @@ data class TitleDetailView(
@ProtoNumber(8) val nonAppearanceInfo: String = "", @ProtoNumber(8) val nonAppearanceInfo: String = "",
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(), @ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(), @ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
@ProtoNumber(14) val isSimulReleased: Boolean = true, @ProtoNumber(14) val isSimulReleased: Boolean = false,
@ProtoNumber(17) val chaptersDescending: Boolean = true @ProtoNumber(17) val chaptersDescending: Boolean = true
) ) {
private val isWebtoon: Boolean
get() = firstChapterList.all(Chapter::isVerticalOnly) &&
lastChapterList.all(Chapter::isVerticalOnly)
private val isOneShot: Boolean
get() = chapterCount == 1 && firstChapterList.firstOrNull()
?.name?.equals("one-shot", true) == true
private val chapterCount: Int
get() = firstChapterList.size + lastChapterList.size
private val isReEdition: Boolean
get() = viewingPeriodDescription.contains(MangaPlus.REEDITION_REGEX)
val isCompleted: Boolean
get() = nonAppearanceInfo.contains(MangaPlus.COMPLETED_REGEX) || isOneShot
val genres: List<String>
get() = listOf(
if (isSimulReleased && !isReEdition) "Simulrelease" else "",
if (isOneShot) "One-shot" else "",
if (isReEdition) "Re-edition" else "",
if (isWebtoon) "Webtoon" else ""
)
}
@Serializable @Serializable
data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList()) data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList())
@ -123,7 +148,8 @@ data class Chapter(
@ProtoNumber(3) val name: String, @ProtoNumber(3) val name: String,
@ProtoNumber(4) val subTitle: String? = null, @ProtoNumber(4) val subTitle: String? = null,
@ProtoNumber(6) val startTimeStamp: Int, @ProtoNumber(6) val startTimeStamp: Int,
@ProtoNumber(7) val endTimeStamp: Int @ProtoNumber(7) val endTimeStamp: Int,
@ProtoNumber(9) val isVerticalOnly: Boolean = false
) )
@Serializable @Serializable