mirror of
https://github.com/recloudstream/cloudstream-extensions.git
synced 2024-08-15 03:03:54 +00:00
Sflix fix
This commit is contained in:
parent
865fcf8dc1
commit
f61cdc0cf7
3 changed files with 237 additions and 141 deletions
|
@ -1,5 +1,5 @@
|
|||
// use an integer for version numbers
|
||||
version = 8
|
||||
version = 9
|
||||
|
||||
|
||||
cloudstream {
|
||||
|
|
|
@ -3,22 +3,23 @@ package com.lagradost
|
|||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
//import com.lagradost.cloudstream3.animeproviders.ZoroProvider
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import com.lagradost.nicehttp.requestCreator
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.net.URI
|
||||
|
@ -28,6 +29,7 @@ import java.util.*
|
|||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
open class SflixProvider : MainAPI() {
|
||||
override var mainUrl = "https://sflix.to"
|
||||
|
@ -356,12 +358,13 @@ open class SflixProvider : MainAPI() {
|
|||
// val extractorData =
|
||||
// "https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling"
|
||||
|
||||
val hasLoadedExtractor = loadExtractor(iframeLink, null, subtitleCallback, callback)
|
||||
if (!hasLoadedExtractor) {
|
||||
if (!loadExtractor(iframeLink, null, subtitleCallback, callback)) {
|
||||
extractRabbitStream(
|
||||
iframeLink,
|
||||
subtitleCallback,
|
||||
callback,
|
||||
false,
|
||||
decryptKey = getKey()
|
||||
) { it }
|
||||
}
|
||||
}
|
||||
|
@ -370,6 +373,10 @@ open class SflixProvider : MainAPI() {
|
|||
return !urls.isNullOrEmpty()
|
||||
}
|
||||
|
||||
// override suspend fun extractorVerifierJob(extractorData: String?) {
|
||||
// runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
|
||||
// }
|
||||
|
||||
private fun Element.toSearchResult(): SearchResponse {
|
||||
val inner = this.selectFirst("div.film-poster")
|
||||
val img = inner!!.select("img")
|
||||
|
@ -450,69 +457,149 @@ open class SflixProvider : MainAPI() {
|
|||
return code.reversed()
|
||||
}
|
||||
|
||||
fun getSourceObject(responseJson: String?, decryptKey: String?): SourceObject? {
|
||||
if (responseJson == null) return null
|
||||
return if (decryptKey != null) {
|
||||
val encryptedMap = tryParseJson<SourceObjectEncrypted>(responseJson)
|
||||
val sources = encryptedMap?.sources
|
||||
|
||||
if (sources == null || encryptedMap.encrypted == false) {
|
||||
tryParseJson(responseJson)
|
||||
} else {
|
||||
val decrypted = decryptMapped<List<Sources>>(sources, decryptKey)
|
||||
SourceObject(
|
||||
sources = decrypted,
|
||||
tracks = encryptedMap.tracks
|
||||
suspend fun getKey(): String? {
|
||||
data class KeyObject(
|
||||
@JsonProperty("key") val key: String? = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
tryParseJson(responseJson)
|
||||
}
|
||||
return app.get("https://raw.githubusercontent.com/BlipBlob/blabflow/main/keys.json")
|
||||
.parsed<KeyObject>().key
|
||||
}
|
||||
|
||||
private fun getSources(
|
||||
socketUrl: String,
|
||||
id: String,
|
||||
callback: suspend (Resource<SourceObject>) -> Unit
|
||||
/**
|
||||
* 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")
|
||||
parseJson<PollingData?>(jsonText)?.let { return it }
|
||||
delay(1000L * i)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new session if the request fails
|
||||
* @return the data and if it is new.
|
||||
* */
|
||||
private suspend fun getUpdatedData(
|
||||
response: NiceResponse,
|
||||
data: PollingData,
|
||||
baseUrl: String
|
||||
): Pair<PollingData, Boolean> {
|
||||
if (!response.okhttpResponse.isSuccessful) {
|
||||
return negotiateNewSid(baseUrl)?.let {
|
||||
it to true
|
||||
} ?: (data to false)
|
||||
}
|
||||
return data to false
|
||||
}
|
||||
|
||||
|
||||
private suspend fun initPolling(
|
||||
extractorData: String,
|
||||
referer: String
|
||||
): Pair<PollingData?, String?> {
|
||||
val headers = mapOf(
|
||||
"Referer" to referer // "https://rabbitstream.net/"
|
||||
)
|
||||
|
||||
val data = negotiateNewSid(extractorData) ?: return null to null
|
||||
app.post(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
||||
requestBody = "40".toRequestBody(),
|
||||
headers = headers
|
||||
)
|
||||
|
||||
// This makes the second get request work, and re-connect work.
|
||||
val reconnectSid =
|
||||
parseJson<PollingData>(
|
||||
app.get(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
||||
headers = headers
|
||||
)
|
||||
// .also { println("First get ${it.text}") }
|
||||
.text.replaceBefore("{", "")
|
||||
).sid
|
||||
|
||||
// This response is used in the post requests. Same contents in all it seems.
|
||||
val authInt =
|
||||
app.get(
|
||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
||||
timeout = 60,
|
||||
headers = headers
|
||||
).text
|
||||
//.also { println("Second get ${it}") }
|
||||
// Dunno if it's actually generated like this, just guessing.
|
||||
.toIntOrNull()?.plus(1) ?: 3
|
||||
|
||||
return data to reconnectSid
|
||||
}
|
||||
|
||||
suspend fun runSflixExtractorVerifierJob(
|
||||
api: MainAPI,
|
||||
extractorData: String?,
|
||||
referer: String
|
||||
) {
|
||||
app.baseClient.newWebSocket(
|
||||
requestCreator("GET", socketUrl),
|
||||
object : WebSocketListener() {
|
||||
val sidRegex = Regex("""sid.*"(.*?)"""")
|
||||
val sourceRegex = Regex("""\{.*\}""")
|
||||
val codeRegex = Regex("""^\d*""")
|
||||
|
||||
var key: String? = null
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
ioSafe {
|
||||
callback(Resource.Failure(false, code, null, reason))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
Log.d("getSources", "onMessage $text")
|
||||
val code = codeRegex.find(text)?.value?.toIntOrNull() ?: return
|
||||
|
||||
when (code) {
|
||||
0 -> webSocket.send("40")
|
||||
40 -> {
|
||||
key = sidRegex.find(text)?.groupValues?.get(1)
|
||||
webSocket.send("""42["getSources",{"id":"$id"}]""")
|
||||
}
|
||||
42 -> {
|
||||
val response = sourceRegex.find(text)?.value
|
||||
val sourceObject = getSourceObject(response, key)
|
||||
val resource = if (sourceObject == null)
|
||||
Resource.Failure(false, null, null, response ?: "")
|
||||
else Resource.Success(sourceObject)
|
||||
ioSafe { callback(resource) }
|
||||
webSocket.close(1005, "41")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extractorData == null) return
|
||||
val headers = mapOf(
|
||||
"Referer" to referer // "https://rabbitstream.net/"
|
||||
)
|
||||
|
||||
lateinit var data: PollingData
|
||||
var reconnectSid = ""
|
||||
|
||||
initPolling(extractorData, referer)
|
||||
.also {
|
||||
data = it.first ?: throw RuntimeException("Data Null")
|
||||
reconnectSid = it.second ?: throw RuntimeException("ReconnectSid Null")
|
||||
}
|
||||
|
||||
// Prevents them from fucking us over with doing a while(true){} loop
|
||||
val interval = maxOf(data.pingInterval?.toLong()?.plus(2000) ?: return, 10000L)
|
||||
var reconnect = false
|
||||
var newAuth = false
|
||||
|
||||
|
||||
while (true) {
|
||||
val authData =
|
||||
when {
|
||||
newAuth -> "40"
|
||||
reconnect -> """42["_reconnect", "$reconnectSid"]"""
|
||||
else -> "3"
|
||||
}
|
||||
|
||||
val url = "${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}"
|
||||
|
||||
getUpdatedData(
|
||||
app.post(url, json = authData, headers = headers),
|
||||
data,
|
||||
extractorData
|
||||
).also {
|
||||
newAuth = it.second
|
||||
data = it.first
|
||||
}
|
||||
|
||||
//.also { println("Sflix post job ${it.text}") }
|
||||
Log.d(api.name, "Running ${api.name} job $url")
|
||||
|
||||
val time = measureTimeMillis {
|
||||
// This acts as a timeout
|
||||
val getResponse = app.get(
|
||||
url,
|
||||
timeout = interval / 1000,
|
||||
headers = headers
|
||||
)
|
||||
// .also { println("Sflix get job ${it.text}") }
|
||||
reconnect = getResponse.text.contains("sid")
|
||||
}
|
||||
// Always waits even if the get response is instant, to prevent a while true loop.
|
||||
if (time < interval - 4000)
|
||||
delay(4000)
|
||||
}
|
||||
}
|
||||
|
||||
// Only scrape servers with these names
|
||||
|
@ -522,8 +609,7 @@ open class SflixProvider : MainAPI() {
|
|||
}
|
||||
|
||||
// For re-use in Zoro
|
||||
private suspend
|
||||
fun Sources.toExtractorLink(
|
||||
private suspend fun Sources.toExtractorLink(
|
||||
caller: MainAPI,
|
||||
name: String,
|
||||
extractorData: String? = null,
|
||||
|
@ -605,10 +691,7 @@ open class SflixProvider : MainAPI() {
|
|||
return currentKey
|
||||
}
|
||||
|
||||
private fun decryptSourceUrl(
|
||||
decryptionKey: ByteArray,
|
||||
sourceUrl: String
|
||||
): String {
|
||||
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")
|
||||
|
@ -624,8 +707,7 @@ open class SflixProvider : MainAPI() {
|
|||
return String(decryptedData, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private inline
|
||||
fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||
return tryParseJson(decrypt(input, key))
|
||||
}
|
||||
|
||||
|
@ -642,28 +724,75 @@ open class SflixProvider : MainAPI() {
|
|||
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 mainIframeUrl =
|
||||
url.substringBeforeLast("/")
|
||||
val mainIframeId = url.substringAfterLast("/")
|
||||
.substringBefore("?") // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> dcPOVRE57YOT
|
||||
// val iframe = app.get(url, referer = mainUrl)
|
||||
// val iframeKey =
|
||||
// iframe.document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||
// .attr("src").substringAfter("render=")
|
||||
// val iframeToken = getCaptchaToken(url, iframeKey)
|
||||
// val number =
|
||||
// Regex("""recaptchaNumber = '(.*?)'""").find(iframe.text)?.groupValues?.get(1)
|
||||
|
||||
var isDone = false
|
||||
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("{", "")
|
||||
|
||||
// Hardcoded for now, does not support Zoro yet.
|
||||
getSources(
|
||||
"wss://wsx.dokicloud.one/socket.io/?EIO=4&transport=websocket",
|
||||
mainIframeId
|
||||
) { sourceResource ->
|
||||
if (sourceResource !is Resource.Success) {
|
||||
isDone = true
|
||||
return@getSources
|
||||
sid = parseJson<PollingData>(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 = sourceResource.value
|
||||
val sourceObject = if (decryptKey != null) {
|
||||
val encryptedMap = response.parsedSafe<SourceObjectEncrypted>()
|
||||
val sources = encryptedMap?.sources
|
||||
if (sources == null || encryptedMap.encrypted == false) {
|
||||
response.parsedSafe()
|
||||
} else {
|
||||
val decrypted = decryptMapped<List<Sources>>(sources, decryptKey)
|
||||
SourceObject(
|
||||
sources = decrypted,
|
||||
tracks = encryptedMap.tracks
|
||||
)
|
||||
}
|
||||
} else {
|
||||
response.parsedSafe()
|
||||
} ?: return@suspendSafeApiCall
|
||||
|
||||
sourceObject.tracks?.forEach { track ->
|
||||
track?.toSubtitleFile()?.let { subtitleFile ->
|
||||
|
@ -683,48 +812,15 @@ open class SflixProvider : MainAPI() {
|
|||
source?.toExtractorLink(
|
||||
this,
|
||||
nameTransformer(subList.second),
|
||||
)?.forEach(callback)
|
||||
extractorData,
|
||||
)
|
||||
?.forEach {
|
||||
// Sets Zoro SID used for video loading
|
||||
// (this as? ZoroProvider)?.sid?.set(it.url.hashCode(), sid)
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
isDone = true
|
||||
}
|
||||
|
||||
var elapsedTime = 0
|
||||
val maxTime = 30
|
||||
|
||||
while (elapsedTime < maxTime && !isDone) {
|
||||
elapsedTime++
|
||||
delay(1_000)
|
||||
}
|
||||
|
||||
//// val iframe = app.get(url, referer = mainUrl)
|
||||
//// val iframeKey =
|
||||
//// iframe.document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
|
||||
//// .attr("src").substringAfter("render=")
|
||||
//// val iframeToken = getCaptchaToken(url, iframeKey)
|
||||
//// val number =
|
||||
//// Regex("""recaptchaNumber = '(.*?)'""").find(iframe.text)?.groupValues?.get(1)
|
||||
//
|
||||
// val sid = null
|
||||
// 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"
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// println("Sflix response: $response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class TwoEmbedProvider : TmdbProvider() {
|
|||
val mappedservers = parseJson<EmbedJson>(ajax)
|
||||
val iframeLink = mappedservers.link
|
||||
if (iframeLink.contains("rabbitstream")) {
|
||||
extractRabbitStream(iframeLink, subtitleCallback, callback) { it }
|
||||
extractRabbitStream(iframeLink, subtitleCallback, callback, false, decryptKey = SflixProvider.getKey()) { it }
|
||||
} else {
|
||||
loadExtractor(iframeLink, embedUrl, subtitleCallback, callback)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue