@@ -10,6 +10,8 @@ import com.lagradost.cloudstream3.utils.loadExtractor
1010import com.lagradost.cloudstream3.utils.newExtractorLink
1111import org.jsoup.Jsoup
1212import org.jsoup.nodes.Element
13+ import org.json.JSONArray
14+ import org.json.JSONObject
1315import java.net.URI
1416import javax.crypto.Cipher
1517import javax.crypto.spec.IvParameterSpec
@@ -257,6 +259,77 @@ class ChikiAnimationProvider : MainAPI() {
257259 val referer = " $RPM /"
258260 val hdr = mapOf (" User-Agent" to UA , " Referer" to referer, " X-Requested-With" to " XMLHttpRequest" )
259261
262+ // The RPM API returns the video config AES-CBC encrypted as a hex blob.
263+ // Key/IV are static in the SPA (verified against multiple episodes).
264+ val rpmKey = " kiemtienmua911ca" .toByteArray(Charsets .UTF_8 )
265+ val rpmIv = " 1234567890oiuytr" .toByteArray(Charsets .UTF_8 )
266+
267+ // Parse a decrypted video config and emit the real playable M3U8 + subtitles
268+ suspend fun addRpmLinks (videoJson : String ): Boolean {
269+ return try {
270+ val obj = JSONObject (videoJson)
271+ val hlsPath = obj.optString(" hlsVideoTiktok" ).ifBlank {
272+ obj.optString(" hlsVideo" ).ifBlank { obj.optString(" hls" ) }
273+ }
274+ if (hlsPath.isBlank() || ! hlsPath.startsWith(" /hls/" )) return false
275+
276+ // streamingConfig -> adjust.<server> -> domain + params (respect order/disabled)
277+ var domain = " "
278+ val params = StringBuilder ()
279+ try {
280+ val sc = JSONObject (obj.getString(" streamingConfig" ))
281+ val adjust = sc.getJSONObject(" adjust" )
282+ val order = sc.optJSONArray(" order" ) ? : JSONArray ()
283+ for (i in 0 until order.length()) {
284+ val prov = order.getString(i)
285+ val cfg = adjust.optJSONObject(prov) ? : continue
286+ if (cfg.optBoolean(" disabled" , false )) continue
287+ val d = cfg.optString(" domain" , " " )
288+ if (d.isBlank()) continue
289+ domain = d
290+ val p = cfg.optJSONObject(" params" )
291+ if (p != null ) {
292+ val it = p.keys()
293+ var first = true
294+ while (it.hasNext()) {
295+ val k = it.next()
296+ if (first) { params.append(" ?" ); first = false } else params.append(" &" )
297+ params.append(k).append(" =" ).append(p.optString(k))
298+ }
299+ }
300+ break
301+ }
302+ } catch (e: Exception ) {}
303+
304+ // Fresh per-response token; required by some streams
305+ val pk = obj.optJSONObject(" pk" )
306+ val k = pk?.optString(" k" , " " ) ? : " "
307+ val kx = pk?.optString(" kx" , " " ) ? : " "
308+ val full = buildString {
309+ append(" $RPM /hlsmod/$domain /${hlsPath.removePrefix(" /hls/" )} " )
310+ append(params)
311+ if (k.isNotBlank()) { append(if (params.isEmpty()) " ?" else " &" ); append(" k=" ).append(k) }
312+ if (kx.isNotBlank()) { append(" &kx=" ).append(kx) }
313+ }
314+
315+ if (addM3u8(full, referer, " Multi Player" )) {
316+ try {
317+ val subs = obj.getJSONObject(" subtitle" )
318+ val sit = subs.keys()
319+ while (sit.hasNext()) {
320+ val lang = sit.next()
321+ val path = subs.optString(lang)
322+ if (path.isNotBlank()) {
323+ subtitleCallback(SubtitleFile (lang, " $RPM${path.substringBefore(" #" )} " ))
324+ }
325+ }
326+ } catch (e: Exception ) {}
327+ return true
328+ }
329+ false
330+ } catch (e: Exception ) { false }
331+ }
332+
260333 // The JS uses fetch('/api/...?id=' + hash) to get the video config as JSON
261334 val endpoints = listOf (
262335 " $RPM /api/v1/video?id=$id " ,
@@ -268,7 +341,14 @@ class ChikiAnimationProvider : MainAPI() {
268341 for (ep in endpoints) {
269342 val res = try { app.get(ep, headers = hdr).text } catch (e: Exception ) { continue }
270343 if (res.isBlank()) continue
271-
344+
345+ // RPM API response is AES-CBC encrypted hex — decrypt it first
346+ val bytes = hexToBytes(res.trim())
347+ if (bytes != null ) {
348+ val decrypted = aesDecrypt(bytes, rpmKey, rpmIv)
349+ if (decrypted != null && addRpmLinks(decrypted)) return true
350+ }
351+
272352 // Direct M3U8 response
273353 if (res.trim().startsWith(" #EXTM3U" )) {
274354 if (addM3u8(ep, referer, " Multi Player" )) return true
0 commit comments