Refactor the MangaPlus code. (#10225)
This commit is contained in:
parent
63e9f627e0
commit
1175d23ac7
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue