LuftVerbot
2023-01-08 12:03:39 +01:00
committed by GitHub
parent 80da44f81f
commit c936474b08
7 changed files with 385 additions and 302 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = '9anime'
pkgNameSuffix = 'en.nineanime'
extClass = '.NineAnime'
extVersionCode = 22
extVersionCode = 23
libVersion = '13'
}

View File

@ -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<String, String> {
val quickJs = QuickJs.create()
val keys = try {
val scriptResult = quickJs.evaluate(finderScript(allJsScript)).toString()
val returnObject = json.decodeFromString<JsonObject>(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 = {};
"""

View File

@ -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<Application>()
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
}
}

View File

@ -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<Application>()
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 = '<iframe src="$embedLink" allow="autoplay; fullscreen" allowfullscreen="yes" scrolling="no" style="width: 100%; height: 100%; overflow: hidden;" frameborder="no"></iframe>';
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
}
}

View File

@ -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<Application>()
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
}
}

View File

@ -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 <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
override fun episodeListParse(response: Response): List<SEpisode> {
val animeUrl = response.request.header("url").toString()
val responseObject = json.decodeFromString<JsonObject>(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<Video> {
val epurl = response.request.header("url").toString()
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
val videoList = mutableListOf<Video>()
@ -110,38 +143,34 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// Sub
document.select("div[data-type=sub] > ul > li[data-sv-id=41]")
.firstOrNull()?.attr("data-link-id")
?.let { videoList.addAll(extractVideo(it, "Sub")) }
?.let { videoList.addAll(extractVideo("Sub", epurl)) }
// Dub
document.select("div[data-type=dub] > ul > li[data-sv-id=41]")
.firstOrNull()?.attr("data-link-id")
?.let { videoList.addAll(extractVideo(it, "Dub")) }
?.let { videoList.addAll(extractVideo("Dub", epurl)) }
return videoList
}
private fun extractVideo(sourceId: String, lang: String): List<Video> {
val vrf = encodeVrf(sourceId)
val episodeBody = network.client.newCall(GET("$baseUrl/ajax/server/$sourceId?vrf=$vrf"))
.execute().body!!.string()
val encryptedSourceUrl = json.decodeFromString<JsonObject>(episodeBody)["result"]!!
.jsonObject["url"]!!.jsonPrimitive.content
val embedLink = decodeVrf(encryptedSourceUrl)
val vizcloudClient = client.newBuilder().addInterceptor(VizcloudInterceptor()).build()
val referer = Headers.headersOf("Referer", "$baseUrl/")
private fun extractVideo(lang: String, epurl: String): List<Video> {
val jsInterceptor = client.newBuilder().addInterceptor(JsInterceptor(lang.lowercase())).build()
val embedLink = jsInterceptor.newCall(GET("$baseUrl$epurl")).execute().request.header("url").toString()
val jsVizInterceptor = client.newBuilder().addInterceptor(JsVizInterceptor(embedLink)).build()
val sourceUrl = jsVizInterceptor.newCall(GET(embedLink, headers = Headers.headersOf("Referer", "$baseUrl/"))).execute().request.header("url").toString()
val referer = Headers.headersOf("referer", embedLink)
val sourceObject = json.decodeFromString<JsonObject>(
vizcloudClient.newCall(GET(embedLink, referer))
client.newCall(GET(sourceUrl, referer))
.execute().body!!.string()
)
val mediaSources = sourceObject["data"]!!.jsonObject["media"]!!.jsonObject["sources"]!!.jsonArray
val masterUrls = mediaSources.map { it.jsonObject["file"]!!.jsonPrimitive.content }
val masterUrl = masterUrls.find { !it.contains("/simple/") } ?: masterUrls.first()
val headers = Headers.headersOf("referer", embedLink, "origin", "https://" + masterUrl.toHttpUrl().topPrivateDomain())
val result = client.newCall(GET(masterUrl, headers)).execute()
val masterUrl = masterUrls.find { it.contains("/simple/") } ?: masterUrls.first()
val result = client.newCall(GET(masterUrl)).execute()
val masterPlaylist = result.body!!.string()
return masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p $lang"
val videoUrl = masterUrl.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl, headers = headers)
Video(videoUrl, quality, videoUrl)
}
}
@ -182,15 +211,19 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return this
}
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.select("div.poster a").attr("href"))
thumbnail_url = element.select("div.poster img").attr("src")
title = element.select("a.name").text()
}
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val vrf = encodeVrf(query)
return GET("$baseUrl/filter?keyword=${encode(query)}&vrf=$vrf&page=$page")
val jsVrfInterceptor = client.newBuilder().addInterceptor(JsVrfInterceptor(query, baseUrl)).build()
val vrf = jsVrfInterceptor.newCall(GET("$baseUrl/filter")).execute().request.header("url").toString()
return GET("$baseUrl/filter?keyword=$query&vrf=$vrf&page=$page", headers = Headers.headersOf("Referer", "$baseUrl/"))
}
override fun animeDetailsParse(document: Document): SAnime {
@ -234,8 +267,8 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("9anime.to", "9anime.id", "9anime.pl")
entryValues = arrayOf("https://9anime.to", "https://9anime.id", "https://9anime.pl")
entries = arrayOf("9anime.to", "9anime.gs", "9anime.pl")
entryValues = arrayOf("https://9anime.to", "https://9anime.gs", "https://9anime.pl")
setDefaultValue("https://9anime.to")
summary = "%s"
@ -280,116 +313,4 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
screen.addPreference(videoQualityPref)
screen.addPreference(videoLanguagePref)
}
private fun encodeVrf(id: String) = encode(encrypt(cipher(encode(id), cipherKey)))
private fun decodeVrf(text: String) = decode(cipher(decrypt(text), decipherKey))
private fun encrypt(input: String): String {
if (input.any { it.code > 255 }) throw Exception("illegal characters!")
var output = ""
for (i in input.indices step 3) {
val a = intArrayOf(-1, -1, -1, -1)
a[0] = input[i].code shr 2
a[1] = (3 and input[i].code) shl 4
if (input.length > i + 1) {
a[1] = a[1] or (input[i + 1].code shr 4)
a[2] = (15 and input[i + 1].code) shl 2
}
if (input.length > i + 2) {
a[2] = a[2] or (input[i + 2].code shr 6)
a[3] = 63 and input[i + 2].code
}
for (n in a) {
if (n == -1) output += "="
else {
if (n in 0..63) output += nineAnimeKey[n]
}
}
}
return output
}
private fun cipher(input: String, cipherKey: String): String {
val arr = IntArray(256) { it }
var u = 0
var r: Int
arr.indices.forEach {
u = (u + arr[it] + cipherKey[it % cipherKey.length].code) % 256
r = arr[it]
arr[it] = arr[u]
arr[u] = r
}
u = 0
var c = 0
return input.indices.map { j ->
c = (c + 1) % 256
u = (u + arr[c]) % 256
r = arr[c]
arr[c] = arr[u]
arr[u] = r
(input[j].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
}.joinToString("")
}
private fun decrypt(input: String): String {
val t = if (input.replace("""[\t\n\f\r]""".toRegex(), "").length % 4 == 0) {
input.replace("""==?$""".toRegex(), "")
} else input
if (t.length % 4 == 1 || t.contains("""[^+/0-9A-Za-z]""".toRegex())) throw Exception("bad input")
var i: Int
var r = ""
var e = 0
var u = 0
for (o in t.indices) {
e = e shl 6
i = nineAnimeKey.indexOf(t[o])
e = e or i
u += 6
if (24 == u) {
r += ((16711680 and e) shr 16).toChar()
r += ((65280 and e) shr 8).toChar()
r += (255 and e).toChar()
e = 0
u = 0
}
}
return if (12 == u) {
e = e shr 4
r + e.toChar()
} else {
if (18 == u) {
e = e shr 2
r += ((65280 and e) shr 8).toChar()
r += (255 and e).toChar()
}
r
}
}
private fun encode(input: String): String = java.net.URLEncoder.encode(input, "utf-8").replace("+", "%20")
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
private val keys by lazy {
val allJsScript = runBlocking {
client.newCall(
GET(
url = "https://s2.bunnycdn.ru/assets/_9anime/min/all.js",
cache = CacheControl.FORCE_NETWORK
)
).execute().body!!.string()
}
getKeys(allJsScript, json)
}
private val cipherKey by lazy {
keys.first
}
private val decipherKey by lazy {
keys.second
}
}
private const val nineAnimeKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

View File

@ -1,88 +0,0 @@
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.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers.Companion.toHeaders
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 VizcloudInterceptor : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("bruh")
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
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 = request.header("User-Agent")
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63\""
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("/mediainfo")) {
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(12, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return newRequest
}
}