Sflix fix

This commit is contained in:
Blatzar 2022-10-09 01:59:38 +02:00
parent 865fcf8dc1
commit f61cdc0cf7
3 changed files with 237 additions and 141 deletions

View File

@ -1,5 +1,5 @@
// use an integer for version numbers // use an integer for version numbers
version = 8 version = 9
cloudstream { cloudstream {

View File

@ -3,22 +3,23 @@ package com.lagradost
import android.util.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer 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.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.nicehttp.requestCreator import com.lagradost.nicehttp.NiceResponse
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import okhttp3.WebSocket import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.WebSocketListener
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.net.URI import java.net.URI
@ -28,6 +29,7 @@ import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.system.measureTimeMillis
open class SflixProvider : MainAPI() { open class SflixProvider : MainAPI() {
override var mainUrl = "https://sflix.to" override var mainUrl = "https://sflix.to"
@ -356,12 +358,13 @@ open class SflixProvider : MainAPI() {
// val extractorData = // val extractorData =
// "https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling" // "https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling"
val hasLoadedExtractor = loadExtractor(iframeLink, null, subtitleCallback, callback) if (!loadExtractor(iframeLink, null, subtitleCallback, callback)) {
if (!hasLoadedExtractor) {
extractRabbitStream( extractRabbitStream(
iframeLink, iframeLink,
subtitleCallback, subtitleCallback,
callback, callback,
false,
decryptKey = getKey()
) { it } ) { it }
} }
} }
@ -370,6 +373,10 @@ open class SflixProvider : MainAPI() {
return !urls.isNullOrEmpty() return !urls.isNullOrEmpty()
} }
// override suspend fun extractorVerifierJob(extractorData: String?) {
// runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
// }
private fun Element.toSearchResult(): SearchResponse { private fun Element.toSearchResult(): SearchResponse {
val inner = this.selectFirst("div.film-poster") val inner = this.selectFirst("div.film-poster")
val img = inner!!.select("img") val img = inner!!.select("img")
@ -450,69 +457,149 @@ open class SflixProvider : MainAPI() {
return code.reversed() return code.reversed()
} }
fun getSourceObject(responseJson: String?, decryptKey: String?): SourceObject? { suspend fun getKey(): String? {
if (responseJson == null) return null data class KeyObject(
return if (decryptKey != null) { @JsonProperty("key") val key: String? = null
val encryptedMap = tryParseJson<SourceObjectEncrypted>(responseJson) )
val sources = encryptedMap?.sources return app.get("https://raw.githubusercontent.com/BlipBlob/blabflow/main/keys.json")
.parsed<KeyObject>().key
if (sources == null || encryptedMap.encrypted == false) {
tryParseJson(responseJson)
} else {
val decrypted = decryptMapped<List<Sources>>(sources, decryptKey)
SourceObject(
sources = decrypted,
tracks = encryptedMap.tracks
)
}
} else {
tryParseJson(responseJson)
}
} }
private fun getSources( /**
socketUrl: String, * Generates a session
id: String, * 1 Get request.
callback: suspend (Resource<SourceObject>) -> Unit * */
) { private suspend fun negotiateNewSid(baseUrl: String): PollingData? {
app.baseClient.newWebSocket( // Tries multiple times
requestCreator("GET", socketUrl), for (i in 1..5) {
object : WebSocketListener() { val jsonText =
val sidRegex = Regex("""sid.*"(.*?)"""") app.get("$baseUrl&t=${generateTimeStamp()}").text.replaceBefore("{", "")
val sourceRegex = Regex("""\{.*\}""") // println("Negotiated sid $jsonText")
val codeRegex = Regex("""^\d*""") parseJson<PollingData?>(jsonText)?.let { return it }
delay(1000L * i)
}
return null
}
var key: String? = 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
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
ioSafe {
callback(Resource.Failure(false, code, null, reason))
}
}
override fun onMessage(webSocket: WebSocket, text: String) { private suspend fun initPolling(
Log.d("getSources", "onMessage $text") extractorData: String,
val code = codeRegex.find(text)?.value?.toIntOrNull() ?: return referer: String
): Pair<PollingData?, String?> {
when (code) { val headers = mapOf(
0 -> webSocket.send("40") "Referer" to referer // "https://rabbitstream.net/"
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")
}
}
}
}
) )
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
) {
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 // Only scrape servers with these names
@ -522,8 +609,7 @@ open class SflixProvider : MainAPI() {
} }
// For re-use in Zoro // For re-use in Zoro
private suspend private suspend fun Sources.toExtractorLink(
fun Sources.toExtractorLink(
caller: MainAPI, caller: MainAPI,
name: String, name: String,
extractorData: String? = null, extractorData: String? = null,
@ -605,10 +691,7 @@ open class SflixProvider : MainAPI() {
return currentKey return currentKey
} }
private fun decryptSourceUrl( private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String {
decryptionKey: ByteArray,
sourceUrl: String
): String {
val cipherData = base64DecodeArray(sourceUrl) val cipherData = base64DecodeArray(sourceUrl)
val encrypted = cipherData.copyOfRange(16, cipherData.size) val encrypted = cipherData.copyOfRange(16, cipherData.size)
val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding") val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding")
@ -624,8 +707,7 @@ open class SflixProvider : MainAPI() {
return String(decryptedData, StandardCharsets.UTF_8) return String(decryptedData, StandardCharsets.UTF_8)
} }
private inline private inline fun <reified T> decryptMapped(input: String, key: String): T? {
fun <reified T> decryptMapped(input: String, key: String): T? {
return tryParseJson(decrypt(input, key)) return tryParseJson(decrypt(input, key))
} }
@ -642,89 +724,103 @@ open class SflixProvider : MainAPI() {
url: String, url: String,
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit, callback: (ExtractorLink) -> Unit,
useSidAuthentication: Boolean,
/** Used for extractorLink name, input: Source name */
extractorData: String? = null,
decryptKey: String? = null,
nameTransformer: (String) -> String, nameTransformer: (String) -> String,
) = suspendSafeApiCall { ) = suspendSafeApiCall {
// https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> https://rapid-cloud.ru/embed-6 // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> https://rapid-cloud.ru/embed-6
// val mainIframeUrl = val mainIframeUrl =
// url.substringBeforeLast("/") url.substringBeforeLast("/")
val mainIframeId = url.substringAfterLast("/") val mainIframeId = url.substringAfterLast("/")
.substringBefore("?") // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> dcPOVRE57YOT .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. sid = parseJson<PollingData>(text).sid
getSources( ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}") }
"wss://wsx.dokicloud.one/socket.io/?EIO=4&transport=websocket",
mainIframeId
) { sourceResource ->
if (sourceResource !is Resource.Success) {
isDone = true
return@getSources
} }
}
val sourceObject = sourceResource.value val getSourcesUrl = "${
mainIframeUrl.replace(
sourceObject.tracks?.forEach { track -> "/embed",
track?.toSubtitleFile()?.let { subtitleFile -> "/ajax/embed"
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"
) )
}/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"
)
)
list.forEach { subList -> val sourceObject = if (decryptKey != null) {
subList.first?.forEach { source -> val encryptedMap = response.parsedSafe<SourceObjectEncrypted>()
source?.toExtractorLink( val sources = encryptedMap?.sources
this, if (sources == null || encryptedMap.encrypted == false) {
nameTransformer(subList.second), response.parsedSafe()
)?.forEach(callback) } 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 ->
subtitleCallback.invoke(subtitleFile)
} }
isDone = true
} }
var elapsedTime = 0 val list = listOf(
val maxTime = 30 sourceObject.sources to "source 1",
sourceObject.sources1 to "source 2",
sourceObject.sources2 to "source 3",
sourceObject.sourcesBackup to "source backup"
)
while (elapsedTime < maxTime && !isDone) { list.forEach { subList ->
elapsedTime++ subList.first?.forEach { source ->
delay(1_000) source?.toExtractorLink(
this,
nameTransformer(subList.second),
extractorData,
)
?.forEach {
// Sets Zoro SID used for video loading
// (this as? ZoroProvider)?.sid?.set(it.url.hashCode(), sid)
callback(it)
}
}
} }
//// 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")
} }
} }
} }

View File

@ -62,7 +62,7 @@ class TwoEmbedProvider : TmdbProvider() {
val mappedservers = parseJson<EmbedJson>(ajax) val mappedservers = parseJson<EmbedJson>(ajax)
val iframeLink = mappedservers.link val iframeLink = mappedservers.link
if (iframeLink.contains("rabbitstream")) { if (iframeLink.contains("rabbitstream")) {
extractRabbitStream(iframeLink, subtitleCallback, callback) { it } extractRabbitStream(iframeLink, subtitleCallback, callback, false, decryptKey = SflixProvider.getKey()) { it }
} else { } else {
loadExtractor(iframeLink, embedUrl, subtitleCallback, callback) loadExtractor(iframeLink, embedUrl, subtitleCallback, callback)
} }