diff --git a/Loklok/build.gradle.kts b/Loklok/build.gradle.kts index 443f84df..531a9d05 100644 --- a/Loklok/build.gradle.kts +++ b/Loklok/build.gradle.kts @@ -6,8 +6,7 @@ cloudstream { language = "en" // All of these properties are optional, you can safely remove them -// description = "#2 best extension based on Loklok API" - description = "Use External Player" + description = "#2 best extension based on Loklok API" authors = listOf("Hexated") /** diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index ce40324f..55a24678 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 99 +version = 100 cloudstream { diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index 35b191ae..95243cf2 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Session import com.google.gson.JsonParser +import com.hexated.RabbitStream.extractRabbitStream import com.lagradost.cloudstream3.extractors.StreamSB import com.lagradost.cloudstream3.extractors.XStreamCdn import com.lagradost.cloudstream3.network.CloudflareKiller @@ -45,17 +46,7 @@ object SoraExtractor : SoraStream() { ).parsedSafe()?.let { source -> val link = source.link ?: return@let if (link.contains("rabbitstream")) { - val rabbitId = link.substringAfterLast("/").substringBefore("?") - app.get( - "https://rabbitstream.net/ajax/embed-5/getSources?id=$rabbitId", - headers = mapOf("X-Requested-With" to "XMLHttpRequest") - ).parsedSafe()?.tracks?.map { sub -> - subtitleCallback.invoke( - SubtitleFile( - sub.label.toString(), sub.file ?: return@map null - ) - ) - } + extractRabbitStream(link, subtitleCallback, callback, false, decryptKey = RabbitStream.getKey()) { it } } else { loadExtractor( link, twoEmbedAPI, subtitleCallback, callback @@ -542,7 +533,7 @@ object SoraExtractor : SoraStream() { quality?.replace(Regex("\\d{3,4}p"), "Noverse")?.replace(".", " ") ?: "Noverse" callback.invoke( ExtractorLink( - name, + "Noverse", name, link, "", @@ -648,7 +639,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "Filmxy $size ($server)", + "Filmxy", "Filmxy $size ($server)", link, "$filmxyAPI/", @@ -1088,7 +1079,7 @@ object SoraExtractor : SoraStream() { if (!ouo.startsWith("https://ouo")) return@apmap null callback.invoke( ExtractorLink( - "AnimeKaizoku [${episodeData.third}]", + "AnimeKaizoku", "AnimeKaizoku [${episodeData.third}]", bypassOuo(ouo) ?: return@apmap null, "$animeKaizokuAPI/", @@ -1248,7 +1239,7 @@ object SoraExtractor : SoraStream() { ?.let { "[$it]" } ?: quality callback.invoke( ExtractorLink( - "UHDMovies $tags $size", + "UHDMovies", "UHDMovies $tags $size", downloadLink ?: return@apmap null, "", @@ -1339,7 +1330,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "GMovies [$size]", + "GMovies", "GMovies [$size]", videoLink ?: return@apmap null, "", @@ -1394,7 +1385,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "FDMovies [$size]", + "FDMovies", "FDMovies [$size]", videoLink ?: return@apmap null, "", @@ -1513,7 +1504,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "TVMovies [${videoData?.second}]", + "TVMovies", "TVMovies [${videoData?.second}]", videoData?.first ?: return, "", @@ -1654,7 +1645,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "Moviesbay $qualityName [$size]", + "Moviesbay", "Moviesbay $qualityName [$size]", link, "", @@ -1752,7 +1743,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "$api $qualityName", + "$api", "$api $qualityName", shortLink ?: return@apmap null, "", @@ -2043,7 +2034,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "Baymovies $tags [$sizeFile]", + "Baymovies", "Baymovies $tags [$sizeFile]", link, "$baymoviesAPI/", @@ -2454,7 +2445,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "$api $tags [$size]", + api, "$api $tags [$size]", path, if(api in needRefererIndex) apiUrl else "", @@ -2497,7 +2488,7 @@ object SoraExtractor : SoraStream() { val tags = getIndexQualityTags(file.name) callback.invoke( ExtractorLink( - "TgarMovies $tags [$size]", + "TgarMovies", "TgarMovies $tags [$size]", "https://api.southkoreacdn.workers.dev/telegram/${file._id}", "$tgarMovieAPI/", @@ -2548,7 +2539,7 @@ object SoraExtractor : SoraStream() { callback.invoke( ExtractorLink( - "GdbotMovies $tags [$size]", + "GdbotMovies", "GdbotMovies $tags [$size]", videoUrl ?: return@apmap null, "", @@ -2597,7 +2588,7 @@ object SoraExtractor : SoraStream() { val size = "%.2f GB".format(bytesToGigaBytes(it.third.toDouble())) callback.invoke( ExtractorLink( - "DahmerMovies $tags [$size]", + "DahmerMovies", "DahmerMovies $tags [$size]", url + it.second, "", diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt index 3867b1c9..49faa873 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -96,7 +96,7 @@ open class SoraStream : TmdbProvider() { const val filmxyAPI = "https://www.filmxy.vip" const val kimcartoonAPI = "https://kimcartoon.li" const val xMovieAPI = "https://xemovies.to" - const val haikeiFlixhqAPI = "https://api.haikei.xyz/movies/flixhq" + const val haikeiFlixhqAPI = "https://api.haikei.xyz/movies/flixhq" // disabled const val consumetZoroAPI = "https://api.consumet.org/anime/zoro" const val consumetCrunchyrollAPI = "https://api.consumet.org/anime/crunchyroll" // dead const val kickassanimeAPI = "https://www2.kickassanime.ro" @@ -471,17 +471,17 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - invokeFlixhq( - res.title, - res.year, - res.season, - res.episode, - res.lastSeason, - subtitleCallback, - callback - ) - }, +// { +// invokeFlixhq( +// res.title, +// res.year, +// res.season, +// res.episode, +// res.lastSeason, +// subtitleCallback, +// callback +// ) +// }, { invokeKisskh(res.title, res.season, res.episode, subtitleCallback, callback) }, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt index c9bb1089..3ff4851e 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt @@ -168,17 +168,17 @@ class SoraStreamLite : SoraStream() { callback ) }, - { - invokeFlixhq( - res.title, - res.year, - res.season, - res.episode, - res.lastSeason, - subtitleCallback, - callback - ) - }, +// { +// invokeFlixhq( +// res.title, +// res.year, +// res.season, +// res.episode, +// res.lastSeason, +// subtitleCallback, +// callback +// ) +// }, { invokeKisskh(res.title, res.season, res.episode, subtitleCallback, callback) }, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index 8708b905..da5cee5d 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -1,6 +1,7 @@ package com.hexated import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.hexated.SoraStream.Companion.base64DecodeAPI import com.hexated.SoraStream.Companion.baymoviesAPI import com.hexated.SoraStream.Companion.consumetCrunchyrollAPI @@ -8,11 +9,14 @@ import com.hexated.SoraStream.Companion.filmxyAPI import com.hexated.SoraStream.Companion.gdbot import com.hexated.SoraStream.Companion.smashyStreamAPI import com.hexated.SoraStream.Companion.tvMoviesAPI +import com.hexated.SoraStream.Companion.twoEmbedAPI import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getCaptchaToken +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.RequestBodyTypes import com.lagradost.nicehttp.requestCreator @@ -26,6 +30,7 @@ import org.jsoup.nodes.Document import java.net.URI import java.net.URL import java.net.URLEncoder +import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.security.SecureRandom import java.util.* @@ -1235,4 +1240,286 @@ object CryptoAES { SecureRandom().nextBytes(this) } } +} + +object RabbitStream { + + suspend fun MainAPI.extractRabbitStream( + url: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + useSidAuthentication: Boolean, + /** Used for extractorLink name, input: Source name */ + extractorData: String? = null, + decryptKey: String? = null, + nameTransformer: (String) -> String, + ) = suspendSafeApiCall { + // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> https://rapid-cloud.ru/embed-6 + val mainIframeUrl = + url.substringBeforeLast("/") + val mainIframeId = url.substringAfterLast("/") + .substringBefore("?") // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> dcPOVRE57YOT + var sid: String? = null + if (useSidAuthentication && extractorData != null) { + negotiateNewSid(extractorData)?.also { pollingData -> + app.post( + "$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}", + requestBody = "40".toRequestBody(), + timeout = 60 + ) + val text = app.get( + "$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}", + timeout = 60 + ).text.replaceBefore("{", "") + + sid = AppUtils.parseJson(text).sid + ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}") } + } + } + val getSourcesUrl = "${ + mainIframeUrl.replace( + "/embed", + "/ajax/embed" + ) + }/getSources?id=$mainIframeId${sid?.let { "$&sId=$it" } ?: ""}" + val response = app.get( + getSourcesUrl, + referer = mainUrl, + headers = mapOf( + "X-Requested-With" to "XMLHttpRequest", + "Accept" to "*/*", + "Accept-Language" to "en-US,en;q=0.5", + "Connection" to "keep-alive", + "TE" to "trailers" + ) + ) + + val sourceObject = if (decryptKey != null) { + val encryptedMap = response.parsedSafe() + val sources = encryptedMap?.sources + if (sources == null || encryptedMap.encrypted == false) { + response.parsedSafe() + } else { + val decrypted = + decryptMapped>(sources, decryptKey) + SourceObject( + sources = decrypted, + tracks = encryptedMap.tracks + ) + } + } else { + response.parsedSafe() + } ?: return@suspendSafeApiCall + + sourceObject.tracks?.forEach { track -> + track?.toSubtitleFile()?.let { subtitleFile -> + subtitleCallback.invoke(subtitleFile) + } + } + + val list = listOf( + sourceObject.sources to "source 1", + sourceObject.sources1 to "source 2", + sourceObject.sources2 to "source 3", + sourceObject.sourcesBackup to "source backup" + ) + + list.forEach { subList -> + subList.first?.forEach { source -> + source?.toExtractorLink( + "Vidcloud", + "$twoEmbedAPI/", + extractorData, + ) + ?.forEach { + // Sets Zoro SID used for video loading +// (this as? ZoroProvider)?.sid?.set(it.url.hashCode(), sid) + callback(it) + } + } + } + } + + private suspend fun Sources.toExtractorLink( + name: String, + referer: String, + extractorData: String? = null, + ): List? { + return this.file?.let { file -> + //println("FILE::: $file") + val isM3u8 = URI(this.file).path.endsWith(".m3u8") || this.type.equals( + "hls", + ignoreCase = true + ) + return if (isM3u8) { + suspendSafeApiCall { + M3u8Helper().m3u8Generation( + M3u8Helper.M3u8Stream( + this.file, + null, + mapOf("Referer" to "https://mzzcloud.life/") + ), false + ) + .map { stream -> + ExtractorLink( + name, + name, + stream.streamUrl, + referer, + getQualityFromName(stream.quality?.toString()), + true, + extractorData = extractorData + ) + } + }.takeIf { !it.isNullOrEmpty() } ?: listOf( + // Fallback if m3u8 extractor fails + ExtractorLink( + name, + name, + this.file, + referer, + getQualityFromName(this.label), + isM3u8, + extractorData = extractorData + ) + ) + } else { + listOf( + ExtractorLink( + name, + name, + file, + referer, + getQualityFromName(this.label), + false, + extractorData = extractorData + ) + ) + } + } + } + + private fun Tracks.toSubtitleFile(): SubtitleFile? { + return this.file?.let { + SubtitleFile( + this.label ?: "Unknown", + it + ) + } + } + + /** + * Generates a session + * 1 Get request. + * */ + private suspend fun negotiateNewSid(baseUrl: String): PollingData? { + // Tries multiple times + for (i in 1..5) { + val jsonText = + app.get("$baseUrl&t=${generateTimeStamp()}").text.replaceBefore( + "{", + "" + ) +// println("Negotiated sid $jsonText") + AppUtils.parseJson(jsonText)?.let { return it } + delay(1000L * i) + } + return null + } + + private fun generateTimeStamp(): String { + val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_" + var code = "" + var time = APIHolder.unixTimeMS + while (time > 0) { + code += chars[(time % (chars.length)).toInt()] + time /= chars.length + } + return code.reversed() + } + + suspend fun getKey(): String? { + return app.get("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt") + .text + } + + private inline fun decryptMapped(input: String, key: String): T? { + return tryParseJson(decrypt(input, key)) + } + + 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") + + Objects.requireNonNull(aesCBC).init( + Cipher.DECRYPT_MODE, SecretKeySpec( + decryptionKey.copyOfRange(0, 32), + "AES" + ), + IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) + ) + val decryptedData = aesCBC!!.doFinal(encrypted) + return String(decryptedData, StandardCharsets.UTF_8) + } + + data class PollingData( + @JsonProperty("sid") val sid: String? = null, + @JsonProperty("upgrades") val upgrades: ArrayList = arrayListOf(), + @JsonProperty("pingInterval") val pingInterval: Int? = null, + @JsonProperty("pingTimeout") val pingTimeout: Int? = null + ) + + data class Tracks( + @JsonProperty("file") val file: String?, + @JsonProperty("label") val label: String?, + @JsonProperty("kind") val kind: String? + ) + + data class Sources( + @JsonProperty("file") val file: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("label") val label: String? + ) + + data class SourceObject( + @JsonProperty("sources") val sources: List? = null, + @JsonProperty("sources_1") val sources1: List? = null, + @JsonProperty("sources_2") val sources2: List? = null, + @JsonProperty("sourcesBackup") val sourcesBackup: List? = null, + @JsonProperty("tracks") val tracks: List? = null + ) + + data class SourceObjectEncrypted( + @JsonProperty("sources") val sources: String?, + @JsonProperty("encrypted") val encrypted: Boolean?, + @JsonProperty("sources_1") val sources1: String?, + @JsonProperty("sources_2") val sources2: String?, + @JsonProperty("sourcesBackup") val sourcesBackup: String?, + @JsonProperty("tracks") val tracks: List? + ) + } \ No newline at end of file