fix(en/nineanime): Fix keys with another repo (#2768)
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Aniwave'
|
extName = 'Aniwave'
|
||||||
extClass = '.Aniwave'
|
extClass = '.Aniwave'
|
||||||
extVersionCode = 63
|
extVersionCode = 64
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
@ -11,4 +11,4 @@ dependencies {
|
|||||||
implementation(project(':lib-mp4upload-extractor'))
|
implementation(project(':lib-mp4upload-extractor'))
|
||||||
implementation(project(':lib-streamtape-extractor'))
|
implementation(project(':lib-streamtape-extractor'))
|
||||||
implementation(project(':lib-playlist-utils'))
|
implementation(project(':lib-playlist-utils'))
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,47 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.nineanime.extractors
|
package eu.kanade.tachiyomi.animeextension.en.nineanime.extractors
|
||||||
|
|
||||||
import android.app.Application
|
import android.util.Base64
|
||||||
import android.os.Handler
|
import app.cash.quickjs.QuickJs
|
||||||
import android.os.Looper
|
|
||||||
import android.webkit.JavascriptInterface
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebSettings
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import eu.kanade.tachiyomi.animeextension.en.nineanime.MediaResponseBody
|
import eu.kanade.tachiyomi.animeextension.en.nineanime.MediaResponseBody
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.util.concurrent.CountDownLatch
|
import javax.crypto.Cipher
|
||||||
import java.util.concurrent.TimeUnit
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
private val cacheControl = CacheControl.Builder().noStore().build()
|
||||||
|
private val noCacheClient = client.newBuilder()
|
||||||
|
.cache(null)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val keys by lazy {
|
||||||
|
noCacheClient.newCall(
|
||||||
|
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
|
||||||
|
).execute().parseAs<List<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
fun videosFromUrl(embedLink: String, name: String, type: String): List<Video> {
|
fun videosFromUrl(embedLink: String, name: String, type: String): List<Video> {
|
||||||
val hosterName = when (name) {
|
val hosterName = when (name) {
|
||||||
"vidplay" -> "VidPlay"
|
"vidplay" -> "VidPlay"
|
||||||
else -> "MyCloud"
|
else -> "MyCloud"
|
||||||
}
|
}
|
||||||
val host = embedLink.toHttpUrl().host
|
val host = embedLink.toHttpUrl().host
|
||||||
val apiSlug = runCatching {
|
val apiUrl = getApiUrl(embedLink, keys)
|
||||||
extractFromUrl(embedLink)
|
|
||||||
}.getOrElse { return emptyList() }
|
|
||||||
|
|
||||||
val apiHeaders = headers.newBuilder().apply {
|
val apiHeaders = headers.newBuilder().apply {
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
@ -46,10 +51,20 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val response = client.newCall(
|
val response = client.newCall(
|
||||||
GET("https://$host/$apiSlug", apiHeaders),
|
GET(apiUrl, apiHeaders),
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
val data = response.parseAs<MediaResponseBody>()
|
val data = runCatching {
|
||||||
|
response.parseAs<MediaResponseBody>()
|
||||||
|
}.getOrElse { // Keys are out of date
|
||||||
|
val newKeys = noCacheClient.newCall(
|
||||||
|
GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl),
|
||||||
|
).execute().parseAs<List<String>>()
|
||||||
|
val newApiUrL = getApiUrl(embedLink, newKeys)
|
||||||
|
client.newCall(
|
||||||
|
GET(newApiUrL, apiHeaders),
|
||||||
|
).execute().parseAs()
|
||||||
|
}
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
data.result.sources.first().file,
|
data.result.sources.first().file,
|
||||||
@ -59,6 +74,69 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getApiUrl(embedLink: String, keyList: List<String>): String {
|
||||||
|
val host = embedLink.toHttpUrl().host
|
||||||
|
val params = embedLink.toHttpUrl().let { url ->
|
||||||
|
url.queryParameterNames.map {
|
||||||
|
Pair(it, url.queryParameter(it) ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||||
|
val encodedID = encodeID(vidId, keyList)
|
||||||
|
val apiSlug = callFromFuToken(host, encodedID)
|
||||||
|
|
||||||
|
return buildString {
|
||||||
|
append("https://")
|
||||||
|
append(host)
|
||||||
|
append("/")
|
||||||
|
append(apiSlug)
|
||||||
|
if (params.isNotEmpty()) {
|
||||||
|
append("?")
|
||||||
|
append(
|
||||||
|
params.joinToString("&") {
|
||||||
|
"${it.first}=${it.second}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeID(videoID: String, keyList: List<String>): String {
|
||||||
|
val rc4Key1 = SecretKeySpec(keyList[0].toByteArray(), "RC4")
|
||||||
|
val rc4Key2 = SecretKeySpec(keyList[1].toByteArray(), "RC4")
|
||||||
|
val cipher1 = Cipher.getInstance("RC4")
|
||||||
|
val cipher2 = Cipher.getInstance("RC4")
|
||||||
|
cipher1.init(Cipher.DECRYPT_MODE, rc4Key1, cipher1.parameters)
|
||||||
|
cipher2.init(Cipher.DECRYPT_MODE, rc4Key2, cipher2.parameters)
|
||||||
|
var encoded = videoID.toByteArray()
|
||||||
|
|
||||||
|
encoded = cipher1.doFinal(encoded)
|
||||||
|
encoded = cipher2.doFinal(encoded)
|
||||||
|
encoded = Base64.encode(encoded, Base64.DEFAULT)
|
||||||
|
return encoded.toString(Charsets.UTF_8).replace("/", "_").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun callFromFuToken(host: String, data: String): String {
|
||||||
|
val fuTokenScript = client.newCall(
|
||||||
|
GET("https://$host/futoken"),
|
||||||
|
).execute().use { it.body.string() }
|
||||||
|
|
||||||
|
val js = buildString {
|
||||||
|
append("(function")
|
||||||
|
append(
|
||||||
|
fuTokenScript.substringAfter("window")
|
||||||
|
.substringAfter("function")
|
||||||
|
.replace("jQuery.ajax(", "")
|
||||||
|
.substringBefore("+location.search"),
|
||||||
|
)
|
||||||
|
append("}(\"$data\"))")
|
||||||
|
}
|
||||||
|
|
||||||
|
return QuickJs.create().use {
|
||||||
|
it.evaluate(js)?.toString()!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
|
private fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
|
||||||
return filter {
|
return filter {
|
||||||
it.kind == "captions"
|
it.kind == "captions"
|
||||||
@ -71,82 +149,4 @@ class VidsrcExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val context: Application by injectLazy()
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractFromUrl(episodeUrl: String): String {
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
|
|
||||||
var webView: WebView? = null
|
|
||||||
|
|
||||||
val jsinterface = JsObject(latch)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
val webview = WebView(context)
|
|
||||||
|
|
||||||
webView = webview
|
|
||||||
with(webview.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
useWideViewPort = false
|
|
||||||
loadWithOverviewMode = false
|
|
||||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
|
||||||
}
|
|
||||||
|
|
||||||
webview.addJavascriptInterface(jsinterface, "ihatetheantichrist")
|
|
||||||
webview.webViewClient = object : WebViewClient() {
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
|
||||||
view?.clearCache(true)
|
|
||||||
view?.clearFormData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
|
||||||
val reqUrl = request.url.toString()
|
|
||||||
if ("futoken" in reqUrl) {
|
|
||||||
return patchScript(reqUrl)
|
|
||||||
}
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
webview.loadUrl(episodeUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(5, TimeUnit.SECONDS)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
webView?.stopLoading()
|
|
||||||
webView?.destroy()
|
|
||||||
webView = null
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsinterface.payload
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchScript(scriptUrl: String): WebResourceResponse {
|
|
||||||
val scriptBody = client.newCall(GET(scriptUrl)).execute().use { it.body.string() }
|
|
||||||
val newBody = scriptBody.replace("return", "ihatetheantichrist.passPayload('mediainfo/'+a.join(',')+location.search);return")
|
|
||||||
return WebResourceResponse(
|
|
||||||
"application/javascript", // mimeType
|
|
||||||
"utf-8", // encoding
|
|
||||||
200, // status code
|
|
||||||
"ok", // reason phrase
|
|
||||||
mapOf( // response headers
|
|
||||||
"server" to "cloudflare",
|
|
||||||
),
|
|
||||||
ByteArrayInputStream(newBody.toByteArray()), // data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user