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
This commit is contained in:
@ -5,7 +5,7 @@ ext {
|
||||
extName = '9anime'
|
||||
pkgNameSuffix = 'en.nineanime'
|
||||
extClass = '.NineAnime'
|
||||
extVersionCode = 22
|
||||
extVersionCode = 23
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -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 = {};
|
||||
|
||||
"""
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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+/"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user