Skip to content

Commit d0682f3

Browse files
Last Sync: 2026-07-08 11:25 (Mobile)
1 parent c67fffd commit d0682f3

1 file changed

Lines changed: 132 additions & 184 deletions

File tree

Lines changed: 132 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
package com.hexated
22

3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.webkit.WebView
6+
import android.webkit.WebViewClient
37
import com.lagradost.cloudstream3.*
48
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
59
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
6-
import com.lagradost.cloudstream3.amap
10+
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
711
import com.lagradost.cloudstream3.utils.ExtractorLink
812
import com.lagradost.cloudstream3.utils.INFER_TYPE
913
import com.lagradost.cloudstream3.utils.Qualities
1014
import com.lagradost.cloudstream3.utils.loadExtractor
1115
import com.lagradost.cloudstream3.utils.newExtractorLink
1216
import kotlinx.coroutines.Dispatchers
1317
import kotlinx.coroutines.delay
18+
import kotlinx.coroutines.suspendCancellableCoroutine
1419
import kotlinx.coroutines.withContext
20+
import kotlinx.coroutines.withTimeoutOrNull
1521
import org.jsoup.Jsoup
1622
import org.jsoup.nodes.Element
17-
import org.mozilla.javascript.BaseFunction
18-
import org.mozilla.javascript.Context
19-
import org.mozilla.javascript.ContextFactory
20-
import org.mozilla.javascript.Function
21-
import org.mozilla.javascript.Scriptable
22-
import org.mozilla.javascript.ScriptableObject
23-
import org.mozilla.javascript.Undefined
2423
import java.util.Calendar
2524

2625
class KuramanimeProvider : MainAPI() {
@@ -31,17 +30,17 @@ class KuramanimeProvider : MainAPI() {
3130
override var lang = "id"
3231
override var sequentialMainPage = true
3332
override val hasDownloadSupport = true
34-
35-
var authorization: String? = null
36-
33+
34+
3735
override val supportedTypes = setOf(
3836
TvType.Anime,
3937
TvType.AnimeMovie,
4038
TvType.OVA
4139
)
4240

4341
companion object {
44-
private var cookies: Map<String, String> = mapOf()
42+
private const val MOBILE_USER_AGENT =
43+
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
4544

4645
fun getType(t: String, s: Int): TvType {
4746
return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA
@@ -176,201 +175,150 @@ class KuramanimeProvider : MainAPI() {
176175
}
177176
}
178177

179-
private suspend fun invokeLocalSource(url: String, server: String, headers: Map<String, String>, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
180-
val request = app.post(url, data = mapOf("authorization" to getAuth()), headers = headers, cookies = cookies)
181-
delay(2000)
182-
val document = request.document
183-
document.select("video#player > source").map {
184-
val link = fixUrl(it.attr("src"))
185-
val quality = it.attr("size").toIntOrNull()
186-
callback.invoke(newExtractorLink(fixTitle(server), fixTitle(server), link, INFER_TYPE) {
187-
this.quality = quality ?: Qualities.Unknown.value
188-
})
189-
}
190-
if (server == "kuramadrive") {
191-
document.select("div#animeDownloadLink a").amap {
192-
loadExtractor(it.attr("href"), "$mainUrl/", subtitleCallback, callback)
178+
@SuppressLint("SetJavaScriptEnabled")
179+
override suspend fun loadLinks(
180+
data: String,
181+
isCasting: Boolean,
182+
subtitleCallback: (SubtitleFile) -> Unit,
183+
callback: (ExtractorLink) -> Unit
184+
): Boolean {
185+
val context = CloudStreamApp.context ?: return false
186+
var found = false
187+
188+
val snapshots = withTimeoutOrNull(150_000L) {
189+
withContext(Dispatchers.Main) {
190+
captureServerSnapshots(context, data)
191+
}
192+
} ?: emptyMap()
193+
194+
snapshots.forEach { (server, html) ->
195+
val doc = Jsoup.parse(html, data)
196+
197+
doc.select("video#player > source").forEach { source ->
198+
val link = fixUrl(source.attr("src"))
199+
if (link.isBlank()) return@forEach
200+
val quality = source.attr("size").toIntOrNull()
201+
found = true
202+
callback.invoke(newExtractorLink(fixTitle(server), fixTitle(server), link, INFER_TYPE) {
203+
this.quality = quality ?: Qualities.Unknown.value
204+
})
193205
}
194-
}
195-
}
196-
197-
override suspend fun loadLinks(data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit): Boolean {
198-
val req = app.get(data)
199-
val res = req.document
200-
cookies = req.cookies
201-
202-
val token = res.selectFirst("meta[name=csrf-token]")?.attr("content") ?: return false
203-
val dataKps = res.selectFirst("div.col-lg-12.mt-3")?.attr("data-kk") ?: return false
204-
205-
val assets = getAssets(dataKps)
206-
var headers = mapOf(
207-
"X-CSRF-TOKEN" to token,
208-
"X-Fuck-ID" to "${assets.MIX_AUTH_KEY}:${assets.MIX_AUTH_TOKEN}",
209-
"X-Request-ID" to randomId(),
210-
"X-Request-Index" to "0",
211-
"X-Requested-With" to "XMLHttpRequest",
212-
)
213-
214-
val tokenRes = app.get("$mainUrl/${assets.MIX_PREFIX_AUTH_ROUTE_PARAM}${assets.MIX_AUTH_ROUTE_PARAM}", headers = headers, cookies = cookies)
215-
val tokenKey = tokenRes.text
216-
cookies = tokenRes.cookies
217206

218-
headers = mapOf("X-CSRF-TOKEN" to token, "X-Requested-With" to "XMLHttpRequest")
207+
doc.selectFirst("div.iframe-container iframe")?.attr("src")?.takeIf { it.isNotBlank() }?.let { iframeSrc ->
208+
runCatching {
209+
loadExtractor(fixUrl(iframeSrc), "$mainUrl/", subtitleCallback) {
210+
found = true
211+
callback.invoke(it)
212+
}
213+
}
214+
}
219215

220-
res.select("select#changeServer option").amap { source ->
221-
val server = source.attr("value")
222-
val link = "$data?${assets.MIX_PAGE_TOKEN_KEY}=$tokenKey&${assets.MIX_STREAM_SERVER_KEY}=$server"
223216
if (server.contains(Regex("(?i)kuramadrive|archive"))) {
224-
invokeLocalSource(link, server, headers, subtitleCallback, callback)
225-
} else {
226-
val request = app.post(link, data = mapOf("authorization" to getAuth()), referer = data, headers = headers, cookies = cookies)
227-
delay(2000)
228-
request.document.select("div.iframe-container iframe").attr("src").let { videoUrl ->
229-
loadExtractor(fixUrl(videoUrl), "$mainUrl/", subtitleCallback, callback)
217+
doc.select("div#animeDownloadLink a[href]").forEach { a ->
218+
val href = a.attr("href")
219+
if (href.isNotBlank()) {
220+
runCatching {
221+
loadExtractor(href, "$mainUrl/", subtitleCallback) {
222+
found = true
223+
callback.invoke(it)
224+
}
225+
}
226+
}
230227
}
231228
}
232229
}
233-
return true
234-
}
235-
236-
private suspend fun getAssets(bpjs: String?): Assets {
237-
val env = app.get("$mainUrl/assets/js/$bpjs.js").text
238-
return Assets(
239-
env.substringAfter("MIX_PREFIX_AUTH_ROUTE_PARAM: '").substringBefore("',"),
240-
env.substringAfter("MIX_AUTH_ROUTE_PARAM: '").substringBefore("',"),
241-
env.substringAfter("MIX_AUTH_KEY: '").substringBefore("',"),
242-
env.substringAfter("MIX_AUTH_TOKEN: '").substringBefore("',"),
243-
env.substringAfter("MIX_PAGE_TOKEN_KEY: '").substringBefore("',"),
244-
env.substringAfter("MIX_STREAM_SERVER_KEY: '").substringBefore("',")
245-
)
246-
}
247230

248-
suspend fun getAuth(): String {
249-
return authorization ?: fetchAuth().also { authorization = it }
231+
return found
250232
}
251233

252-
suspend fun fetchAuth(): String {
253-
val url = "$mainUrl/storage/leviathan.js?v=${System.currentTimeMillis()}"
254-
val res = app.get(url).text
255-
return withContext(Dispatchers.IO) { extractAuthToken(res) } ?: throw ErrorLoadingException()
256-
}
257-
258-
private fun extractAuthToken(rawScript: String): String? {
259-
val script = rawScript
260-
.replace(Regex("""\basync\s+function"""), "function")
261-
.replace(Regex("""\bawait\s+"""), "")
262-
263-
var captured: String? = null
264-
265-
fun captureFromOptions(options: Any?) {
266-
if (captured != null) return
267-
val opts = options as? Scriptable ?: return
268-
val headers = ScriptableObject.getProperty(opts, "headers") as? Scriptable ?: return
269-
val authVal = ScriptableObject.getProperty(headers, "Authorization")
270-
if (authVal != Scriptable.NOT_FOUND && authVal != null) {
271-
captured = Context.toString(authVal).removePrefix("Bearer ").trim()
272-
}
234+
private suspend fun captureServerSnapshots(context: Context, url: String): Map<String, String> {
235+
val snapshots = linkedMapOf<String, String>()
236+
val webView = try {
237+
WebView(context)
238+
} catch (_: Throwable) {
239+
return snapshots
273240
}
274241

275-
val factory = object : ContextFactory() {
276-
override fun observeInstructionCount(cx: Context, instructionCount: Int) {
277-
if (Thread.currentThread().isInterrupted) {
278-
throw InterruptedException("aborted")
279-
}
280-
}
281-
282-
override fun makeContext(): Context {
283-
val cx = super.makeContext()
284-
cx.instructionObserverThreshold = 2000
285-
return cx
242+
try {
243+
webView.settings.apply {
244+
javaScriptEnabled = true
245+
domStorageEnabled = true
246+
userAgentString = MOBILE_USER_AGENT
286247
}
287-
}
288-
289-
val worker = Thread(null, {
290-
val rhino = factory.enterContext()
291-
try {
292-
rhino.optimizationLevel = -1 // interpreted mode: avoids JVM method-size limits on big scripts + enables the kill switch above
293-
rhino.setLanguageVersion(Context.VERSION_ES6)
294-
val scope: Scriptable = rhino.initSafeStandardObjects()
295248

296-
val noop = object : BaseFunction() {
297-
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array<Any?>): Any =
298-
Undefined.instance
299-
}
300-
val fetchStub = object : BaseFunction() {
301-
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array<Any?>): Any {
302-
captureFromOptions(args.getOrNull(1))
303-
return Undefined.instance
304-
}
305-
}
306-
val ajaxStub = object : BaseFunction() {
307-
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array<Any?>): Any {
308-
captureFromOptions(args.getOrNull(0))
309-
return Undefined.instance
249+
val loaded = withTimeoutOrNull(25_000L) {
250+
suspendCancellableCoroutine<Unit> { cont ->
251+
webView.webViewClient = object : WebViewClient() {
252+
override fun onPageFinished(view: WebView?, finishedUrl: String?) {
253+
if (cont.isActive) cont.resume(Unit)
254+
}
310255
}
256+
webView.loadUrl(url)
311257
}
312-
val dollarStub = object : BaseFunction() {
313-
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array<Any?>): Any = scope
258+
}
259+
if (loaded == null) return snapshots
260+
261+
delay(3000)
262+
263+
val servers = readServerList(webView)
264+
val loopDeadline = System.currentTimeMillis() + 100_000L
265+
266+
for (server in servers) {
267+
if (System.currentTimeMillis() > loopDeadline) break
268+
269+
val switched = withTimeoutOrNull(5_000L) {
270+
webView.evalJs(
271+
"""
272+
(function(){
273+
var el = document.getElementById('changeServer');
274+
if(!el) return false;
275+
el.value = ${server.toJsStringLiteral()};
276+
el.dispatchEvent(new Event('change', {bubbles:true}));
277+
return true;
278+
})();
279+
""".trimIndent()
280+
)
314281
}
315-
ScriptableObject.putProperty(dollarStub, "ajax", ajaxStub)
316-
317-
scope.put("fetch", scope, fetchStub)
318-
scope.put("$", scope, dollarStub)
319-
scope.put("setInterval", scope, noop)
320-
scope.put("setTimeout", scope, noop)
321-
scope.put("clearInterval", scope, noop)
322-
scope.put("clearTimeout", scope, noop)
323-
scope.put("document", scope, rhino.newObject(scope))
324-
scope.put("navigator", scope, rhino.newObject(scope))
325-
scope.put("window", scope, scope)
326-
327-
val preExisting = scope.ids.toSet()
328-
329-
rhino.evaluateString(scope, script, "leviathan.js", 1, null)
330-
331-
val dummyOpts = rhino.newObject(scope)
332-
val candidateArgs = arrayOf<Any?>(
333-
"$mainUrl/probe", "POST", "json", dummyOpts, noop, noop, noop
334-
)
335-
336-
for (id in scope.ids) {
337-
if (captured != null) break
338-
val name = id as? String ?: continue
339-
if (name in preExisting) continue
340-
val fn = ScriptableObject.getProperty(scope, name) as? Function ?: continue
341-
try {
342-
fn.call(rhino, scope, scope, candidateArgs)
343-
} catch (_: Throwable) {
344-
}
282+
if (switched?.contains("true") != true) continue
283+
284+
delay(3500)
285+
286+
val rawHtml = withTimeoutOrNull(5_000L) { webView.evalJs("document.documentElement.outerHTML") }
287+
val html = rawHtml?.let { tryParseJson<String>(it) }
288+
if (!html.isNullOrBlank()) {
289+
snapshots[server] = html
345290
}
346-
} catch (_: Throwable) {
347-
} finally {
348-
Context.exit()
349291
}
350-
}, "kuramanime-leviathan-js", 8 * 1024 * 1024)
351-
352-
worker.isDaemon = true
353-
worker.start()
354-
worker.join(10_000L)
355-
if (worker.isAlive) {
356-
worker.interrupt()
357-
worker.join(1_000L)
292+
} catch (_: Throwable) {
293+
} finally {
294+
webView.stopLoading()
295+
webView.destroy()
358296
}
359297

360-
return captured
298+
return snapshots
361299
}
362300

363-
private fun randomId(length: Int = 6): String {
364-
val allowedChars = ('a'..'z') + ('A'..'Z') + ('0'..'9')
365-
return (1..length).map { allowedChars.random() }.joinToString("")
301+
private suspend fun readServerList(webView: WebView): List<String> {
302+
val raw = withTimeoutOrNull(5_000L) {
303+
webView.evalJs(
304+
"JSON.stringify(Array.from(document.querySelectorAll('#changeServer option')).map(function(o){return o.value}))"
305+
)
306+
}
307+
val innerJson = raw?.let { tryParseJson<String>(it) }
308+
val servers = innerJson?.let { tryParseJson<List<String>>(it) }?.filter { it.isNotBlank() }
309+
return if (servers.isNullOrEmpty()) listOf("kuramadrive") else servers
366310
}
367311

368-
data class Assets(
369-
val MIX_PREFIX_AUTH_ROUTE_PARAM: String?,
370-
val MIX_AUTH_ROUTE_PARAM: String?,
371-
val MIX_AUTH_KEY: String?,
372-
val MIX_AUTH_TOKEN: String?,
373-
val MIX_PAGE_TOKEN_KEY: String?,
374-
val MIX_STREAM_SERVER_KEY: String?,
375-
)
312+
private fun String.toJsStringLiteral(): String =
313+
"\"" + this.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
314+
315+
private suspend fun WebView.evalJs(script: String): String? = suspendCancellableCoroutine { cont ->
316+
try {
317+
evaluateJavascript(script) { result ->
318+
if (cont.isActive) cont.resume(result)
319+
}
320+
} catch (_: Throwable) {
321+
if (cont.isActive) cont.resume(null)
322+
}
323+
}
376324
}

0 commit comments

Comments
 (0)