From c936474b086817ba64a7e17dbfbeb101b3572b5c Mon Sep 17 00:00:00 2001 From: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com> Date: Sun, 8 Jan 2023 12:03:39 +0100 Subject: [PATCH] 9Anime: Fix (#1142) Closes https://github.com/jmir1/aniyomi-extensions/issues/1110 Closes https://github.com/jmir1/aniyomi-extensions/issues/1078 Closes https://github.com/jmir1/aniyomi-extensions/issues/825 Closes https://github.com/jmir1/aniyomi-extensions/issues/781 Closes https://github.com/jmir1/aniyomi-extensions/issues/771 Closes https://github.com/jmir1/aniyomi-extensions/issues/761 Closes https://github.com/jmir1/aniyomi-extensions/issues/733 Closes https://github.com/jmir1/aniyomi-extensions/issues/713 Closes https://github.com/jmir1/aniyomi-extensions/issues/697 --- src/en/nineanime/build.gradle | 2 +- .../animeextension/en/nineanime/FindKeys.kt | 69 ------ .../en/nineanime/JsInterceptor.kt | 105 +++++++++ .../en/nineanime/JsVizInterceptor.kt | 105 +++++++++ .../en/nineanime/JsVrfInterceptor.kt | 109 +++++++++ .../animeextension/en/nineanime/NineAnime.kt | 209 ++++++------------ .../en/nineanime/VizcloudInterceptor.kt | 88 -------- 7 files changed, 385 insertions(+), 302 deletions(-) delete mode 100644 src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/FindKeys.kt create mode 100644 src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsInterceptor.kt create mode 100644 src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVizInterceptor.kt create mode 100644 src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVrfInterceptor.kt delete mode 100644 src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/VizcloudInterceptor.kt diff --git a/src/en/nineanime/build.gradle b/src/en/nineanime/build.gradle index cf92fbcb1..0e95885b1 100644 --- a/src/en/nineanime/build.gradle +++ b/src/en/nineanime/build.gradle @@ -5,7 +5,7 @@ ext { extName = '9anime' pkgNameSuffix = 'en.nineanime' extClass = '.NineAnime' - extVersionCode = 22 + extVersionCode = 23 libVersion = '13' } diff --git a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/FindKeys.kt b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/FindKeys.kt deleted file mode 100644 index a139d248e..000000000 --- a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/FindKeys.kt +++ /dev/null @@ -1,69 +0,0 @@ -package eu.kanade.tachiyomi.animeextension.en.nineanime - -import app.cash.quickjs.QuickJs -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonPrimitive - -const val fallbackcipherKey = "mcYrOtBqfRISotfM" -const val fallbackdecipherKey = "hlPeNwkncH0fq9so" - -fun getKeys(allJsScript: String, json: Json): Pair { - val quickJs = QuickJs.create() - val keys = try { - val scriptResult = quickJs.evaluate(finderScript(allJsScript)).toString() - val returnObject = json.decodeFromString(scriptResult) - val cipherKey = returnObject["cipher"]!!.jsonPrimitive.content - val decipherKey = returnObject["decipher"]!!.jsonPrimitive.content - Pair(cipherKey, decipherKey) - } catch (t: Throwable) { - Pair(fallbackcipherKey, fallbackdecipherKey) - } - quickJs.close() - return keys -} - -private fun finderScript(script: String) = """ -let secret0 = ""; -let secret1 = ""; -const script = String.raw`$script`; -const prefix = `$prefix`; -var newscript = prefix + script; -const fn_regex = /or(?=:function\(.*?\) {var \w=.*?return .;})/gm -let fn_name = script.match(fn_regex); -const regex = RegExp(String.raw`(?<=this\["${'$'}{fn_name}"]\().+?(?=,)`, "gm"); -let res = [...script.matchAll(regex)]; -for (var index of [1,0]) { - let match = res[index][0]; - let varnames = match.split("+"); - for (var varnameindex = 0; varnameindex < varnames.length; varnameindex++) { - let varname = varnames[varnameindex]; - let search = `${'$'}{varname}=`; - // variables are declared on line 2 - let line2index = script.indexOf("\n") + prefix.length; - let line2 = newscript.substring(line2index + 1); - let i = line2index + line2.indexOf(search) + search.length; - let after = newscript.substring(i + 1); - let j = after.indexOf(";") + i + 1; - let before = newscript.substring(0, j + 1); - let after_semicolon = newscript.substring(j + 1); - newscript = before + `secret${'$'}{index}=${'$'}{res[index][0]};` + after_semicolon; - } -}; -try { eval(newscript); } catch(e) {} -let return_object = {cipher: secret0, decipher: secret1}; -JSON.stringify(return_object); -""" - -private const val prefix = """const document = { documentElement: {} }; -const jQuery = function () { return { off: function () { return { on: function(e) { return { on: function() { return { on: function() { return { on: function() { return { on: function() { return { }; } }; } }; } }; } }; } }; }, ready: function (e) {} } }; -jQuery.fn = { dropdown: {}, extend: {} }; -const window = { fn: { extend: {} } }; -const navigator = {}; -const setTimeout = {}; -const clearTimeout = {}; -const setInterval = {}; -const clearInterval = {}; - -""" diff --git a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsInterceptor.kt b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsInterceptor.kt new file mode 100644 index 000000000..b4062d88c --- /dev/null +++ b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsInterceptor.kt @@ -0,0 +1,105 @@ +package eu.kanade.tachiyomi.animeextension.en.nineanime + +import android.annotation.SuppressLint +import android.app.Application +import android.os.Handler +import android.os.Looper +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import eu.kanade.tachiyomi.network.GET +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class JsInterceptor(private val lang: String) : Interceptor { + + private val context = Injekt.get() + private val handler by lazy { Handler(Looper.getMainLooper()) } + + class JsObject(private val latch: CountDownLatch, var payload: String = "") { + @JavascriptInterface + fun passPayload(passedPayload: String) { + payload = passedPayload + latch.countDown() + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Please reload Episode List") + + return chain.proceed(newRequest) + } + + @SuppressLint("SetJavaScriptEnabled") + private fun resolveWithWebView(request: Request): Request? { + + val latch = CountDownLatch(1) + + var webView: WebView? = null + + val origRequestUrl = request.url.toString() + + val jsinterface = JsObject(latch) + + //JavaSrcipt gets the Dub or Sub link of vidstream + val jsScript = """ + (function(){ + setTimeout(function(){ + let el = document.querySelector('div[data-type="$lang"] ul li[data-sv-id="41"]'); + let e = document.createEvent('HTMLEvents'); + e.initEvent('click',true,true); + el.dispatchEvent(e); + setTimeout(function(){ + const resources = performance.getEntriesByType('resource'); + resources.forEach((entry) => { + if(entry.name.includes("https://vidstream.pro/embed/")){ + window.android.passPayload(entry.name); + } + }); + }, 2000); + }, 1000); + })();""" + + val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() + + var newRequest: Request? = null + + handler.post { + val webview = WebView(context) + webView = webview + with(webview.settings) { + javaScriptEnabled = true + domStorageEnabled = true + databaseEnabled = true + useWideViewPort = false + loadWithOverviewMode = false + userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0" + webview.addJavascriptInterface(jsinterface, "android") + webview.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view?.evaluateJavascript(jsScript) {} + } + } + webView?.loadUrl(origRequestUrl, headers) + } + } + + latch.await(12, TimeUnit.SECONDS) + + handler.post { + webView?.stopLoading() + webView?.destroy() + webView = null + } + newRequest = GET(request.url.toString(), headers = Headers.headersOf("url", jsinterface.payload)) + return newRequest + } +} diff --git a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVizInterceptor.kt b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVizInterceptor.kt new file mode 100644 index 000000000..5d6b4173e --- /dev/null +++ b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVizInterceptor.kt @@ -0,0 +1,105 @@ +package eu.kanade.tachiyomi.animeextension.en.nineanime + +import android.annotation.SuppressLint +import android.app.Application +import android.os.Handler +import android.os.Looper +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import eu.kanade.tachiyomi.network.GET +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class JsVizInterceptor(private val embedLink: String) : Interceptor { + + private val context = Injekt.get() + private val handler by lazy { Handler(Looper.getMainLooper()) } + + class JsObject(private val latch: CountDownLatch, var payload: String = "") { + @JavascriptInterface + fun passPayload(passedPayload: String) { + payload = passedPayload + latch.countDown() + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Please reload Episode List") + + return chain.proceed(newRequest) + } + + @SuppressLint("SetJavaScriptEnabled") + private fun resolveWithWebView(request: Request): Request? { + + val latch = CountDownLatch(1) + + var webView: WebView? = null + + val origRequestUrl = request.url.toString() + + val jsinterface = JsObject(latch) + + //JavaSrcipt creates Iframe on vidstream page to bypass iframe-cors and gets the sourceUrl + val jsScript = """ + (function(){ + const html = ''; + document.body.innerHTML += html; + setTimeout(function() { + const iframe = document.querySelector('iframe'); + const entries = iframe.contentWindow.performance.getEntries(); + entries.forEach((entry) => { + if(entry.initiatorType.includes("xmlhttprequest")){ + if(!entry.name.includes("/ping/") && !entry.name.includes("/assets/")){ + window.android.passPayload(entry.name); + } + } + }); + }, 2000); + })(); + """ + + val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() + + var newRequest: Request? = null + + handler.post { + val webview = WebView(context) + webView = webview + with(webview.settings) { + javaScriptEnabled = true + domStorageEnabled = true + databaseEnabled = true + useWideViewPort = false + loadWithOverviewMode = false + userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0" + webview.addJavascriptInterface(jsinterface, "android") + webview.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view?.evaluateJavascript(jsScript) {} + } + } + webView?.loadUrl(origRequestUrl, headers) + } + } + + latch.await(12, TimeUnit.SECONDS) + + handler.post { + webView?.stopLoading() + webView?.destroy() + webView = null + } + newRequest = GET(request.url.toString(), headers = Headers.headersOf("url", jsinterface.payload)) + return newRequest + } +} diff --git a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVrfInterceptor.kt b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVrfInterceptor.kt new file mode 100644 index 000000000..96b1c6f63 --- /dev/null +++ b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVrfInterceptor.kt @@ -0,0 +1,109 @@ +package eu.kanade.tachiyomi.animeextension.en.nineanime + +import android.annotation.SuppressLint +import android.app.Application +import android.os.Handler +import android.os.Looper +import android.webkit.JavascriptInterface +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import eu.kanade.tachiyomi.network.GET +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class JsVrfInterceptor(private val query: String, private val baseUrl: String) : Interceptor { + + private val context = Injekt.get() + private val handler by lazy { Handler(Looper.getMainLooper()) } + + class JsObject(private val latch: CountDownLatch, var payload: String = "") { + @JavascriptInterface + fun passPayload(passedPayload: String) { + payload = passedPayload + latch.countDown() + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Please reload Episode List") + + return chain.proceed(newRequest) + } + + @SuppressLint("SetJavaScriptEnabled") + private fun resolveWithWebView(request: Request): Request? { + + val latch = CountDownLatch(1) + + var webView: WebView? = null + + val origRequestUrl = request.url.toString() + + val jsinterface = JsObject(latch) + + // JavaScript uses search of 9Anime to convert IDs & Querys to the VRF-Key + val jsScript = """ + (function() { + document.querySelector("form.filters input.form-control").value = '$query'; + let inputElemente = document.querySelector('form.filters input.form-control'); + let e = document.createEvent('HTMLEvents'); + e.initEvent('keyup', true, true); + inputElemente.dispatchEvent(e); + window.android.passPayload(document.querySelector('form.filters input[type="hidden"]').value); + })(); + """ + + val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() + + var newRequest: Request? = null + + handler.post { + val webview = WebView(context) + webView = webview + with(webview.settings) { + javaScriptEnabled = true + domStorageEnabled = true + databaseEnabled = true + useWideViewPort = false + loadWithOverviewMode = false + userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0" + + webview.addJavascriptInterface(jsinterface, "android") + + webview.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + if (request?.url.toString().contains("$baseUrl/filter")) { + return super.shouldOverrideUrlLoading(view, request) + } else { + // Block the request + return true + } + } + override fun onPageFinished(view: WebView?, url: String?) { + view?.evaluateJavascript(jsScript) {} + } + } + webView?.loadUrl(origRequestUrl, headers) + } + } + + latch.await(12, TimeUnit.SECONDS) + + handler.post { + webView?.stopLoading() + webView?.destroy() + webView = null + } + newRequest = GET(request.url.toString(), headers = Headers.headersOf("url", jsinterface.payload, "user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0")) + return newRequest + } +} diff --git a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/NineAnime.kt b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/NineAnime.kt index 528429ba3..b80ae77fb 100644 --- a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/NineAnime.kt +++ b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/NineAnime.kt @@ -12,6 +12,9 @@ import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json @@ -19,7 +22,6 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import okhttp3.CacheControl import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient @@ -68,41 +70,72 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override fun episodeListRequest(anime: SAnime): Request { val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup().selectFirst("div[data-id]").attr("data-id") - val vrf = encodeVrf(id) - return GET("$baseUrl/ajax/episode/list/$id?vrf=$vrf") + val jsVrfInterceptor = client.newBuilder().addInterceptor(JsVrfInterceptor(id, baseUrl)).build() + val vrf = jsVrfInterceptor.newCall(GET("$baseUrl/filter")).execute().request.header("url").toString() + return GET("$baseUrl/ajax/episode/list/$id?vrf=$vrf", headers = Headers.headersOf("url", anime.url)) } + + private fun Iterable.parallelMap(f: suspend (A) -> B): List = + runBlocking { + map { async(Dispatchers.Default) { f(it) } }.awaitAll() + } override fun episodeListParse(response: Response): List { + val animeUrl = response.request.header("url").toString() val responseObject = json.decodeFromString(response.body!!.string()) val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content)) - return document.select(episodeListSelector()).map(::episodeFromElement).reversed() + val episodeElements = document.select(episodeListSelector()) + return episodeElements.parallelMap { episodeFromElements(it, animeUrl) } } override fun episodeListSelector() = "div.episodes ul > li > a" - override fun episodeFromElement(element: Element): SEpisode { + private fun episodeFromElements(element: Element, url: String): SEpisode { val episode = SEpisode.create() val epNum = element.attr("data-num") val ids = element.attr("data-ids") val sub = element.attr("data-sub").toInt().toBoolean() val dub = element.attr("data-dub").toInt().toBoolean() - val vrf = encodeVrf(ids) - episode.url = "/ajax/server/list/$ids?vrf=$vrf" + episode.url = "/ajax/server/list/$ids?vrf=&epurl=$url/ep-$epNum" episode.episode_number = epNum.toFloat() - val langPrefix = "[" + if (sub) { "Sub" } else { "" } + if (dub) { ",Dub" } else { "" } + "]" + val langPrefix = "[" + if (sub) { + "Sub" + } else { + "" + } + if (dub) { + ",Dub" + } else { + "" + } + "]" val name = element.parent()?.select("span.d-title")?.text().orEmpty() val namePrefix = "Episode $epNum" episode.name = "Episode $epNum" + if (sub || dub) { ": $langPrefix" - } else { "" } + if (name.isNotEmpty() && name != namePrefix) { + } else { + "" + } + if (name.isNotEmpty() && name != namePrefix) { " $name" - } else { "" } + } else { + "" + } return episode } + override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used") + private fun Int.toBoolean() = this == 1 + override fun videoListRequest(episode: SEpisode): Request { + val ids = episode.url.substringAfter("list/").substringBefore("?vrf") + val jsVrfInterceptor = client.newBuilder().addInterceptor(JsVrfInterceptor(ids, baseUrl)).build() + val vrf = jsVrfInterceptor.newCall(GET("$baseUrl/filter")).execute().request.header("url").toString() + val url = "/ajax/server/list/$ids?vrf=$vrf" + val epurl = episode.url.substringAfter("epurl=") + return GET(baseUrl + url, headers = Headers.headersOf("url", epurl)) + } + override fun videoListParse(response: Response): List