Remove Gson workaround and add rate limiting to MangaPlus. (#9295)

This commit is contained in:
Alessandro Jean 2021-10-01 06:36:14 -03:00 committed by GitHub
parent 6202608920
commit 8a32ad1961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 354 deletions

View File

@ -6,7 +6,11 @@ ext {
extName = 'MANGA Plus by SHUEISHA'
pkgNameSuffix = 'all.mangaplus'
extClass = '.MangaPlusFactory'
extVersionCode = 24
extVersionCode = 25
}
dependencies {
implementation project(':lib-ratelimit')
}
apply from: "$rootDir/common.gradle"

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.extension.all.mangaplus
import android.app.Application
import android.content.SharedPreferences
import android.os.Build
import com.google.gson.Gson
import com.squareup.duktape.Duktape
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
@ -28,6 +26,7 @@ import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.UUID
import java.util.concurrent.TimeUnit
import androidx.preference.CheckBoxPreference as AndroidXCheckBoxPreference
import androidx.preference.ListPreference as AndroidXListPreference
import androidx.preference.PreferenceScreen as AndroidXPreferenceScreen
@ -51,17 +50,11 @@ abstract class MangaPlus(
.add("Session-Token", UUID.randomUUID().toString())
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor { imageIntercept(it) }
.addInterceptor { thumbnailIntercept(it) }
.addInterceptor(::imageIntercept)
.addInterceptor(::thumbnailIntercept)
.addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
.build()
private val protobufJs: String by lazy {
val request = GET(PROTOBUFJS_CDN, headers)
client.newCall(request).execute().body!!.string()
}
private val gson: Gson by lazy { Gson() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
@ -423,25 +416,7 @@ abstract class MangaPlus(
}
private fun Response.asProto(): MangaPlusResponse {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
return ProtoBuf.decodeFromByteArray(body!!.bytes())
// The kotlinx.serialization library eventually always have some issues with
// devices with Android version below Nougat. So, if the device is running Android 6.x,
// the deserialization is done using ProtobufJS + Duktape + Gson.
val bytes = body!!.bytes()
val messageBytes = "var BYTE_ARR = new Uint8Array([${bytes.joinToString()}]);"
val res = Duktape.create().use {
// The current Kotlin version brokes Duktape's module feature,
// so we need to provide an workaround to prevent the usage of 'require'.
it.evaluate("var module = { exports: true };")
it.evaluate(protobufJs)
it.evaluate(messageBytes + DECODE_SCRIPT) as String
}
return gson.fromJson(res, MangaPlusResponse::class.java)
return ProtoBuf.decodeFromByteArray(body!!.bytes())
}
companion object {
@ -451,8 +426,6 @@ abstract class MangaPlus(
private val HEX_GROUP = "(.{1,2})".toRegex()
private const val PROTOBUFJS_CDN = "https://cdn.jsdelivr.net/npm/protobufjs@6.10.1/dist/light/protobuf.js"
private const val RESOLUTION_PREF_KEY = "imageResolution"
private const val RESOLUTION_PREF_TITLE = "Image resolution"
private val RESOLUTION_PREF_ENTRIES = arrayOf("Low resolution", "Medium resolution", "High resolution")

View File

@ -1,320 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangaplus
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class MangaPlusResponse(
@ProtoNumber(1) val success: SuccessResult? = null,
@ProtoNumber(2) val error: ErrorResult? = null
)
@Serializable
data class ErrorResult(
@ProtoNumber(1) val action: Action,
@ProtoNumber(2) val englishPopup: Popup,
@ProtoNumber(3) val spanishPopup: Popup
)
enum class Action { DEFAULT, UNAUTHORIZED, MAINTAINENCE, GEOIP_BLOCKING }
@Serializable
data class Popup(
@ProtoNumber(1) val subject: String,
@ProtoNumber(2) val body: String
)
@Serializable
data class SuccessResult(
@ProtoNumber(1) val isFeaturedUpdated: Boolean? = false,
@ProtoNumber(6) val titleRankingView: TitleRankingView? = null,
@ProtoNumber(8) val titleDetailView: TitleDetailView? = null,
@ProtoNumber(10) val mangaViewer: MangaViewer? = null,
@ProtoNumber(25) val allTitlesViewV2: AllTitlesViewV2? = null,
@ProtoNumber(31) val webHomeViewV3: WebHomeViewV3? = null
)
@Serializable
data class TitleRankingView(@ProtoNumber(1) val titles: List<Title> = emptyList())
@Serializable
data class AllTitlesViewV2(
@ProtoNumber(1) val allTitlesGroup: List<AllTitlesGroup> = emptyList()
)
@Serializable
data class AllTitlesGroup(
@ProtoNumber(1) val theTitle: String,
@ProtoNumber(2) val titles: List<Title> = emptyList()
)
@Serializable
data class WebHomeViewV3(@ProtoNumber(2) val groups: List<UpdatedTitleV2Group> = emptyList())
@Serializable
data class TitleDetailView(
@ProtoNumber(1) val title: Title,
@ProtoNumber(2) val titleImageUrl: String,
@ProtoNumber(3) val overview: String,
@ProtoNumber(4) val backgroundImageUrl: String,
@ProtoNumber(5) val nextTimeStamp: Int = 0,
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
@ProtoNumber(7) val viewingPeriodDescription: String = "",
@ProtoNumber(8) val nonAppearanceInfo: String = "",
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
@ProtoNumber(14) val isSimulReleased: Boolean = true,
@ProtoNumber(17) val chaptersDescending: Boolean = true
)
enum class UpdateTiming { NOT_REGULARLY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY, DAY }
@Serializable
data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList())
@Serializable
data class Title(
@ProtoNumber(1) val titleId: Int,
@ProtoNumber(2) val name: String,
@ProtoNumber(3) val author: String,
@ProtoNumber(4) val portraitImageUrl: String,
@ProtoNumber(5) val landscapeImageUrl: String,
@ProtoNumber(6) val viewCount: Int = 0,
@ProtoNumber(7) val language: Language? = Language.ENGLISH
)
@Serializable
enum class Language(val id: Int) {
@ProtoNumber(0)
@SerializedName("0")
ENGLISH(0),
@ProtoNumber(1)
@SerializedName("1")
SPANISH(1),
@ProtoNumber(2)
@SerializedName("2")
FRENCH(2),
@ProtoNumber(3)
@SerializedName("3")
INDONESIAN(4),
@ProtoNumber(4)
@SerializedName("4")
PORTUGUESE_BR(4),
@ProtoNumber(5)
@SerializedName("5")
RUSSIAN(5),
@ProtoNumber(6)
@SerializedName("6")
THAI(6)
}
@Serializable
data class UpdatedTitleV2Group(
@ProtoNumber(1) val groupName: String,
@ProtoNumber(2) val titleGroups: List<OriginalTitleGroup> = emptyList()
)
@Serializable
data class OriginalTitleGroup(
@ProtoNumber(1) val theTitle: String,
@ProtoNumber(3) val titles: List<UpdatedTitle> = emptyList()
)
@Serializable
data class UpdatedTitle(@ProtoNumber(1) val title: Title)
@Serializable
data class Chapter(
@ProtoNumber(1) val titleId: Int,
@ProtoNumber(2) val chapterId: Int,
@ProtoNumber(3) val name: String,
@ProtoNumber(4) val subTitle: String? = null,
@ProtoNumber(6) val startTimeStamp: Int,
@ProtoNumber(7) val endTimeStamp: Int
)
@Serializable
data class MangaPlusPage(@ProtoNumber(1) val page: MangaPage? = null)
@Serializable
data class MangaPage(
@ProtoNumber(1) val imageUrl: String,
@ProtoNumber(2) val width: Int,
@ProtoNumber(3) val height: Int,
@ProtoNumber(5) val encryptionKey: String? = null
)
// Used for the deserialization on older devices.
const val DECODE_SCRIPT: String =
"""
var protobuf = module.exports;
var Root = protobuf.Root;
var Type = protobuf.Type;
var Field = protobuf.Field;
var Enum = protobuf.Enum;
var OneOf = protobuf.OneOf;
var Response = new Type("Response")
.add(
new OneOf("data")
.add(new Field("success", 1, "SuccessResult"))
.add(new Field("error", 2, "ErrorResult"))
);
var ErrorResult = new Type("ErrorResult")
.add(new Field("action", 1, "Action"))
.add(new Field("englishPopup", 2, "Popup"))
.add(new Field("spanishPopup", 3, "Popup"));
var Action = new Enum("Action")
.add("DEFAULT", 0)
.add("UNAUTHORIZED", 1)
.add("MAINTAINENCE", 2)
.add("GEOIP_BLOCKING", 3);
var Popup = new Type("Popup")
.add(new Field("subject", 1, "string"))
.add(new Field("body", 2, "string"));
var SuccessResult = new Type("SuccessResult")
.add(new Field("isFeaturedUpdated", 1, "bool"))
.add(
new OneOf("data")
.add(new Field("titleRankingView", 6, "TitleRankingView"))
.add(new Field("titleDetailView", 8, "TitleDetailView"))
.add(new Field("mangaViewer", 10, "MangaViewer"))
.add(new Field("allTitlesViewV2", 25, "AllTitlesViewV2"))
.add(new Field("webHomeViewV3", 31, "WebHomeViewV3"))
);
var TitleRankingView = new Type("TitleRankingView")
.add(new Field("titles", 1, "Title", "repeated"));
var AllTitlesViewV2 = new Type("AllTitlesViewV2")
.add(new Field("allTitlesGroup", 1, "AllTitlesGroup", "repeated"));
var AllTitlesGroup = new Type("AllTitlesGroup")
.add(new Field("theTitle", 1, "string"))
.add(new Field("titles", 2, "Title", "repeated"));
var WebHomeViewV3 = new Type("WebHomeViewV3")
.add(new Field("groups", 2, "UpdatedTitleV2Group", "repeated"));
var TitleDetailView = new Type("TitleDetailView")
.add(new Field("title", 1, "Title"))
.add(new Field("titleImageUrl", 2, "string"))
.add(new Field("overview", 3, "string"))
.add(new Field("backgroundImageUrl", 4, "string"))
.add(new Field("nextTimeStamp", 5, "uint32"))
.add(new Field("updateTiming", 6, "UpdateTiming"))
.add(new Field("viewingPeriodDescription", 7, "string"))
.add(new Field("nonAppearanceInfo", 8, "string", {"default": ""}))
.add(new Field("firstChapterList", 9, "Chapter", "repeated"))
.add(new Field("lastChapterList", 10, "Chapter", "repeated"))
.add(new Field("isSimulReleased", 14, "bool"))
.add(new Field("chaptersDescending", 17, "bool"));
var UpdateTiming = new Enum("UpdateTiming")
.add("NOT_REGULARLY", 0)
.add("MONDAY", 1)
.add("TUESDAY", 2)
.add("WEDNESDAY", 3)
.add("THURSDAY", 4)
.add("FRIDAY", 5)
.add("SATURDAY", 6)
.add("SUNDAY", 7)
.add("DAY", 8);
var MangaViewer = new Type("MangaViewer")
.add(new Field("pages", 1, "Page", "repeated"));
var Title = new Type("Title")
.add(new Field("titleId", 1, "uint32"))
.add(new Field("name", 2, "string"))
.add(new Field("author", 3, "string"))
.add(new Field("portraitImageUrl", 4, "string"))
.add(new Field("landscapeImageUrl", 5, "string"))
.add(new Field("viewCount", 6, "uint32", {"default": 0}))
.add(new Field("language", 7, "Language", {"default": 0}));
var Language = new Enum("Language")
.add("ENGLISH", 0)
.add("SPANISH", 1)
.add("FRENCH", 2)
.add("INDONESIAN", 3)
.add("PORTUGUESE_BR", 4)
.add("RUSSIAN", 5)
.add("THAI", 6);
var UpdatedTitleV2Group = new Type("UpdatedTitleV2Group")
.add(new Field("groupName", 1, "string"))
.add(new Field("titleGroups", 2, "OriginalTitleGroup", "repeated"));
var OriginalTitleGroup = new Type("OriginalTitleGroup")
.add(new Field("theTitle", 1, "string"))
.add(new Field("titles", 3, "UpdatedTitle", "repeated"));
var UpdatedTitle = new Type("UpdatedTitle")
.add(new Field("title", 1, "Title"))
.add(new Field("chapterId", 2, "uint32"))
.add(new Field("chapterName", 3, "string"))
.add(new Field("chapterSubtitle", 4, "string"));
var Chapter = new Type("Chapter")
.add(new Field("titleId", 1, "uint32"))
.add(new Field("chapterId", 2, "uint32"))
.add(new Field("name", 3, "string"))
.add(new Field("subTitle", 4, "string", "optional"))
.add(new Field("startTimeStamp", 6, "uint32"))
.add(new Field("endTimeStamp", 7, "uint32"));
var Page = new Type("Page")
.add(new Field("page", 1, "MangaPage"));
var MangaPage = new Type("MangaPage")
.add(new Field("imageUrl", 1, "string"))
.add(new Field("width", 2, "uint32"))
.add(new Field("height", 3, "uint32"))
.add(new Field("encryptionKey", 5, "string", "optional"));
var root = new Root()
.define("mangaplus")
.add(Response)
.add(ErrorResult)
.add(Action)
.add(Popup)
.add(SuccessResult)
.add(TitleRankingView)
.add(AllTitlesViewV2)
.add(AllTitlesGroup)
.add(WebHomeViewV3)
.add(TitleDetailView)
.add(UpdateTiming)
.add(MangaViewer)
.add(Title)
.add(Language)
.add(UpdatedTitleV2Group)
.add(OriginalTitleGroup)
.add(UpdatedTitle)
.add(Chapter)
.add(Page)
.add(MangaPage);
function decode(arr) {
var Response = root.lookupType("Response");
var message = Response.decode(arr);
return Response.toObject(message, {defaults: true});
}
(function () {
return JSON.stringify(decode(BYTE_ARR)).replace(/\,\{\}/g, "");
})();
"""

View File

@ -0,0 +1,138 @@
package eu.kanade.tachiyomi.extension.all.mangaplus
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class MangaPlusResponse(
@ProtoNumber(1) val success: SuccessResult? = null,
@ProtoNumber(2) val error: ErrorResult? = null
)
@Serializable
data class ErrorResult(
@ProtoNumber(2) val englishPopup: Popup,
@ProtoNumber(3) val spanishPopup: Popup
)
@Serializable
data class Popup(
@ProtoNumber(1) val subject: String,
@ProtoNumber(2) val body: String
)
@Serializable
data class SuccessResult(
@ProtoNumber(1) val isFeaturedUpdated: Boolean? = false,
@ProtoNumber(6) val titleRankingView: TitleRankingView? = null,
@ProtoNumber(8) val titleDetailView: TitleDetailView? = null,
@ProtoNumber(10) val mangaViewer: MangaViewer? = null,
@ProtoNumber(25) val allTitlesViewV2: AllTitlesViewV2? = null,
@ProtoNumber(31) val webHomeViewV3: WebHomeViewV3? = null
)
@Serializable
data class TitleRankingView(@ProtoNumber(1) val titles: List<Title> = emptyList())
@Serializable
data class AllTitlesViewV2(
@ProtoNumber(1) val allTitlesGroup: List<AllTitlesGroup> = emptyList()
)
@Serializable
data class AllTitlesGroup(
@ProtoNumber(1) val theTitle: String,
@ProtoNumber(2) val titles: List<Title> = emptyList()
)
@Serializable
data class WebHomeViewV3(@ProtoNumber(2) val groups: List<UpdatedTitleV2Group> = emptyList())
@Serializable
data class TitleDetailView(
@ProtoNumber(1) val title: Title,
@ProtoNumber(2) val titleImageUrl: String,
@ProtoNumber(3) val overview: String,
@ProtoNumber(4) val backgroundImageUrl: String,
@ProtoNumber(5) val nextTimeStamp: Int = 0,
@ProtoNumber(7) val viewingPeriodDescription: String = "",
@ProtoNumber(8) val nonAppearanceInfo: String = "",
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
@ProtoNumber(14) val isSimulReleased: Boolean = true,
@ProtoNumber(17) val chaptersDescending: Boolean = true
)
@Serializable
data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList())
@Serializable
data class Title(
@ProtoNumber(1) val titleId: Int,
@ProtoNumber(2) val name: String,
@ProtoNumber(3) val author: String,
@ProtoNumber(4) val portraitImageUrl: String,
@ProtoNumber(5) val landscapeImageUrl: String,
@ProtoNumber(6) val viewCount: Int = 0,
@ProtoNumber(7) val language: Language? = Language.ENGLISH
)
@Serializable
enum class Language(val id: Int) {
@ProtoNumber(0)
ENGLISH(0),
@ProtoNumber(1)
SPANISH(1),
@ProtoNumber(2)
FRENCH(2),
@ProtoNumber(3)
INDONESIAN(4),
@ProtoNumber(4)
PORTUGUESE_BR(4),
@ProtoNumber(5)
RUSSIAN(5),
@ProtoNumber(6)
THAI(6)
}
@Serializable
data class UpdatedTitleV2Group(
@ProtoNumber(1) val groupName: String,
@ProtoNumber(2) val titleGroups: List<OriginalTitleGroup> = emptyList()
)
@Serializable
data class OriginalTitleGroup(
@ProtoNumber(1) val theTitle: String,
@ProtoNumber(3) val titles: List<UpdatedTitle> = emptyList()
)
@Serializable
data class UpdatedTitle(@ProtoNumber(1) val title: Title)
@Serializable
data class Chapter(
@ProtoNumber(1) val titleId: Int,
@ProtoNumber(2) val chapterId: Int,
@ProtoNumber(3) val name: String,
@ProtoNumber(4) val subTitle: String? = null,
@ProtoNumber(6) val startTimeStamp: Int,
@ProtoNumber(7) val endTimeStamp: Int
)
@Serializable
data class MangaPlusPage(@ProtoNumber(1) val page: MangaPage? = null)
@Serializable
data class MangaPage(
@ProtoNumber(1) val imageUrl: String,
@ProtoNumber(2) val width: Int,
@ProtoNumber(3) val height: Int,
@ProtoNumber(5) val encryptionKey: String? = null
)