11package com.hexated
22
3+ import android.annotation.SuppressLint
4+ import android.content.Context
5+ import android.webkit.WebView
6+ import android.webkit.WebViewClient
37import com.lagradost.cloudstream3.*
48import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
59import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
6- import com.lagradost.cloudstream3.amap
10+ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
711import com.lagradost.cloudstream3.utils.ExtractorLink
812import com.lagradost.cloudstream3.utils.INFER_TYPE
913import com.lagradost.cloudstream3.utils.Qualities
1014import com.lagradost.cloudstream3.utils.loadExtractor
1115import com.lagradost.cloudstream3.utils.newExtractorLink
1216import kotlinx.coroutines.Dispatchers
1317import kotlinx.coroutines.delay
18+ import kotlinx.coroutines.suspendCancellableCoroutine
1419import kotlinx.coroutines.withContext
20+ import kotlinx.coroutines.withTimeoutOrNull
1521import org.jsoup.Jsoup
1622import 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
2423import java.util.Calendar
2524
2625class 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