Fix chapters not loading in Toptoon+. (#10717)

This commit is contained in:
Alessandro Jean 2022-02-05 14:45:07 -03:00 committed by GitHub
parent c51bafdd5f
commit 6da96c40d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 164 additions and 65 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'TOPTOON+'
pkgNameSuffix = 'en.toptoonplus'
extClass = '.TopToonPlus'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View File

@ -19,12 +19,10 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.UUID
import java.util.concurrent.TimeUnit
class TopToonPlus : HttpSource() {
@ -38,7 +36,8 @@ class TopToonPlus : HttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(TopToonPlusWebViewInterceptor(baseUrl, headersBuilder().build()))
.addInterceptor(TopToonPlusTokenInterceptor(baseUrl, headersBuilder().build()))
.addInterceptor(TopToonPlusViewerInterceptor(baseUrl, headersBuilder().build()))
.addInterceptor(RateLimitInterceptor(2, 1, TimeUnit.SECONDS))
.build()
@ -49,8 +48,6 @@ class TopToonPlus : HttpSource() {
.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.US)!!
.toUpperCase(Locale.US)
private val deviceId: String by lazy { UUID.randomUUID().toString() }
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
@ -58,6 +55,7 @@ class TopToonPlus : HttpSource() {
override fun popularMangaRequest(page: Int): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Language", lang)
.add("UA", "web")
.add("X-Api-Key", API_KEY)
.build()
@ -86,6 +84,7 @@ class TopToonPlus : HttpSource() {
override fun latestUpdatesRequest(page: Int): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Language", lang)
.add("UA", "web")
.add("X-Api-Key", API_KEY)
.build()
@ -156,6 +155,7 @@ class TopToonPlus : HttpSource() {
private fun mangaDetailsApiRequest(mangaUrl: String): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Language", lang)
.add("UA", "web")
.add("X-Api-Key", API_KEY)
.build()
@ -216,7 +216,7 @@ class TopToonPlus : HttpSource() {
override fun pageListRequest(chapter: SChapter): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Language", "en")
.add("Language", lang)
.add("UA", "web")
.add("X-Api-Key", API_KEY)
.build()
@ -251,6 +251,11 @@ class TopToonPlus : HttpSource() {
val viewerRequest = viewerRequest(usableEpisode.comicId, usableEpisode.episodeId)
val viewerResponse = client.newCall(viewerRequest).execute()
if (!viewerResponse.isSuccessful) {
throw Exception(COULD_NOT_GET_CHAPTER_IMAGES)
}
val viewerResult = viewerResponse.parseAs<TopToonDetails>()
return viewerResult.data!!.episode
@ -262,6 +267,7 @@ class TopToonPlus : HttpSource() {
private fun viewerRequest(comicId: Int, episodeId: Int): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Language", lang)
.add("UA", "web")
.add("X-Api-Key", API_KEY)
.build()
@ -308,6 +314,7 @@ class TopToonPlus : HttpSource() {
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
private const val COULD_NOT_PARSE_RESPONSE = "Could not parse the API response."
private const val COULD_NOT_GET_CHAPTER_IMAGES = "Could not get the chapter images."
private const val CHAPTER_NOT_FREE = "This chapter is not free to read."
private val DATE_FORMATTER by lazy {

View File

@ -5,9 +5,13 @@ import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
@ -19,24 +23,21 @@ import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* WebView interceptor to get the access token from the user.
* It was created because the website started to use reCAPTCHA.
*/
class TopToonPlusWebViewInterceptor(
private val baseUrl: String,
private val headers: Headers
) : Interceptor {
abstract class TopToonPlusWebViewInterceptor : Interceptor {
private val handler by lazy { Handler(Looper.getMainLooper()) }
protected abstract val baseUrl: String
private val windowKey: String by lazy {
protected abstract val headers: Headers
protected open val executeJavascript: Boolean = true
protected val windowKey: String by lazy {
UUID.randomUUID().toString().replace("-", "")
}
private var token: String? = null
protected val handler by lazy { Handler(Looper.getMainLooper()) }
internal class JsInterface(private val latch: CountDownLatch, var payload: String = "") {
protected class JsInterface(private val latch: CountDownLatch, var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
@ -44,6 +45,72 @@ class TopToonPlusWebViewInterceptor(
}
}
abstract override fun intercept(chain: Interceptor.Chain): Response
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
protected fun proceedWithWebView(websiteRequest: Request): String? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val requestUrl = websiteRequest.url.toString()
val headers = websiteRequest.headers.toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
.toMutableMap()
val userAgent = headers["User-Agent"]
val jsInterface = JsInterface(latch)
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = userAgent.orEmpty().ifEmpty { userAgentString }
}
if (executeJavascript) {
webview.addJavascriptInterface(jsInterface, windowKey)
}
webview.webViewClient = createWebViewClient(jsInterface)
webview.loadUrl(requestUrl, headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.postDelayed({ webView?.destroy() }, DELAY_MILLIS)
if (jsInterface.payload.isBlank()) {
return null
}
return jsInterface.payload
}
protected abstract fun createWebViewClient(jsInterface: JsInterface): WebViewClient
companion object {
private const val TIMEOUT_SEC: Long = 20
private const val DELAY_MILLIS: Long = 10 * 1000
}
}
/**
* WebView interceptor to get the access token from the user.
* It was created because the website started to use reCAPTCHA.
*/
class TopToonPlusTokenInterceptor(
override val baseUrl: String,
override val headers: Headers
) : TopToonPlusWebViewInterceptor() {
private var token: String? = null
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
@ -88,51 +155,10 @@ class TopToonPlusWebViewInterceptor(
return chain.proceed(request)
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun proceedWithWebView(websiteRequest: Request): String? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val requestUrl = websiteRequest.url.toString()
val headers = websiteRequest.headers.toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
.toMutableMap()
val userAgent = headers["User-Agent"]
val jsInterface = JsInterface(latch)
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = userAgent.orEmpty().ifEmpty { userAgentString }
}
webview.addJavascriptInterface(jsInterface, windowKey)
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String?) {
view.evaluateJavascript(createScript()) {}
}
}
webview.loadUrl(requestUrl, headers)
override fun createWebViewClient(jsInterface: JsInterface): WebViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String?) {
view.evaluateJavascript(createScript()) {}
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.postDelayed({ webView?.destroy() }, DELAY_MILLIS)
if (jsInterface.payload.isBlank()) {
return null
}
return jsInterface.payload
}
private fun createScript(): String = """
@ -155,9 +181,75 @@ class TopToonPlusWebViewInterceptor(
window["$windowKey"].passPayload(accessToken || "");
})();
""".trimIndent()
}
/**
* WebView interceptor to get the viewer token for the chapter.
* It was created because the website started to use reCAPTCHA.
*/
class TopToonPlusViewerInterceptor(
override val baseUrl: String,
override val headers: Headers
) : TopToonPlusWebViewInterceptor() {
override val executeJavascript: Boolean = false
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!request.url.toString().startsWith(TopToonPlus.API_URL)) {
return chain.proceed(request)
}
if (request.url.pathSegments.joinToString("/") != VIEWER_ENDPOINT) {
return chain.proceed(request)
}
val comicId = request.url.queryParameter("comicId")!!
val episodeId = request.url.queryParameter("episodeId")!!
val chapterRequest = GET("$baseUrl/comic/$comicId/$episodeId", headers)
val urlWithToken: String
try {
urlWithToken = proceedWithWebView(chapterRequest).orEmpty()
.ifEmpty { request.url.toString() }
} catch (e: Exception) {
throw IOException(e.message)
}
request = request.newBuilder()
.url(urlWithToken)
.build()
return chain.proceed(request)
}
override fun createWebViewClient(jsInterface: JsInterface): WebViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
): WebResourceResponse? {
if (!request.url.toString().contains(VIEWER_ENDPOINT)) {
return null
}
val badResponse = buildJsonObject {
put("action", "unusable comic")
put("message", "not allowed")
put("uuid", UUID.randomUUID().toString())
}
jsInterface.passPayload(request.url.toString())
return WebResourceResponse(
"application/json",
"utf-8",
badResponse.toString().byteInputStream()
)
}
}
companion object {
private const val TIMEOUT_SEC: Long = 20
private const val DELAY_MILLIS: Long = 10 * 1000
private const val VIEWER_ENDPOINT = "api/v1/page/viewer"
}
}