diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt new file mode 100644 index 00000000..b686f7d8 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -0,0 +1,170 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +// No License found in https://github.com/enimax-anime/key +// special credits to @enimax for providing key +class Megacloud : Rabbitstream() { + override val name = "Megacloud" + override val mainUrl = "https://megacloud.tv" + override val embed = "embed-2/ajax/e-1" + override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt" +} + +class Dokicloud : Rabbitstream() { + override val name = "Dokicloud" + override val mainUrl = "https://dokicloud.one" +} + +open class Rabbitstream : ExtractorApi() { + override val name = "Rabbitstream" + override val mainUrl = "https://rabbitstream.net" + override val requiresReferer = false + open val embed = "ajax/embed-4" + open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt" + private var rawKey: String? = null + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = url.substringAfterLast("/").substringBefore("?") + + val response = app.get( + "$mainUrl/$embed/getSources?id=$id", + referer = mainUrl, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ) + + val encryptedMap = response.parsedSafe() + val sources = encryptedMap?.sources + val decryptedSources = if (sources == null || encryptedMap.encrypted == false) { + response.parsedSafe() + } else { + val (key, encData) = extractRealKey(sources, getRawKey()) + val decrypted = decryptMapped>(encData, key) + SourcesResponses( + sources = decrypted, + tracks = encryptedMap.tracks + ) + } + + decryptedSources?.sources?.map { source -> + M3u8Helper.generateM3u8( + name, + source?.file ?: return@map, + "$mainUrl/", + ).forEach(callback) + } + + decryptedSources?.tracks?.map { track -> + subtitleCallback.invoke( + SubtitleFile( + track?.label ?: "", + track?.file ?: return@map + ) + ) + } + + } + + private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it } + + private fun extractRealKey(originalString: String?, stops: String): Pair { + val table = parseJson>>(stops) + val decryptedKey = StringBuilder() + var offset = 0 + var encryptedString = originalString + + table.forEach { (start, end) -> + decryptedKey.append(encryptedString?.substring(start - offset, end - offset)) + encryptedString = encryptedString?.substring( + 0, + start - offset + ) + encryptedString?.substring(end - offset) + offset += end - start + } + return decryptedKey.toString() to encryptedString.toString() + } + + private inline fun decryptMapped(input: String, key: String): T? { + val decrypt = decrypt(input, key) + return AppUtils.tryParseJson(decrypt) + } + + private fun decrypt(input: String, key: String): String { + return decryptSourceUrl( + generateKey( + base64DecodeArray(input).copyOfRange(8, 16), + key.toByteArray() + ), input + ) + } + + private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray { + var key = md5(secret + salt) + var currentKey = key + while (currentKey.size < 48) { + key = md5(key + secret + salt) + currentKey += key + } + return currentKey + } + + private fun md5(input: ByteArray): ByteArray { + return MessageDigest.getInstance("MD5").digest(input) + } + + private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String { + val cipherData = base64DecodeArray(sourceUrl) + val encrypted = cipherData.copyOfRange(16, cipherData.size) + val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding") + aesCBC.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"), + IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) + ) + val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") + return String(decryptedData, StandardCharsets.UTF_8) + } + + data class Tracks( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, + ) + + data class Sources( + @JsonProperty("file") val file: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("label") val label: String? = null, + ) + + data class SourcesResponses( + @JsonProperty("sources") val sources: List? = emptyList(), + @JsonProperty("tracks") val tracks: List? = emptyList(), + ) + + data class SourcesEncrypted( + @JsonProperty("sources") val sources: String? = null, + @JsonProperty("encrypted") val encrypted: Boolean? = null, + @JsonProperty("tracks") val tracks: List? = emptyList(), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ed190bcc..83c61542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -428,7 +428,10 @@ val extractorApis: MutableList = arrayListOf( Cda(), Dailymotion(), ByteShare(), - Ztreamhub() + Ztreamhub(), + Rabbitstream(), + Dokicloud(), + Megacloud(), )