Skip to content

Commit 9fec118

Browse files
committed
fix: resolve Multi Player (RPM) server by decrypting AES-CBC API response
1 parent 7189453 commit 9fec118

1 file changed

Lines changed: 81 additions & 1 deletion

File tree

ChikiAnimationProvider/src/main/kotlin/com/chikianimation/ChikiAnimationProvider.kt

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import com.lagradost.cloudstream3.utils.loadExtractor
1010
import com.lagradost.cloudstream3.utils.newExtractorLink
1111
import org.jsoup.Jsoup
1212
import org.jsoup.nodes.Element
13+
import org.json.JSONArray
14+
import org.json.JSONObject
1315
import java.net.URI
1416
import javax.crypto.Cipher
1517
import 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

Comments
 (0)