Fix chapters not loading in Toptoon+. (#10717)
This commit is contained in:
parent
c51bafdd5f
commit
6da96c40d2
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'TOPTOON+'
|
||||
pkgNameSuffix = 'en.toptoonplus'
|
||||
extClass = '.TopToonPlus'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue