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
if (sources == null || encryptedMap.encrypted == false) {
tryParseJson(responseJson)
} else {
val decrypted = decryptMapped<List<Sources>>(sources, decryptKey)
SourceObject(
sources = decrypted,
tracks = encryptedMap.tracks
) )
} return app.get("https://raw.githubusercontent.com/BlipBlob/blabflow/main/keys.json")
} else { .parsed<KeyObject>().key
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? {
// 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( if (extractorData == null) return
requestCreator("GET", socketUrl), val headers = mapOf(
object : WebSocketListener() { "Referer" to referer // "https://rabbitstream.net/"
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")
}
}
}
}
) )
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,28 +724,75 @@ 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 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 -> sourceObject.tracks?.forEach { track ->
track?.toSubtitleFile()?.let { subtitleFile -> track?.toSubtitleFile()?.let { subtitleFile ->
@ -683,48 +812,15 @@ open class SflixProvider : MainAPI() {
source?.toExtractorLink( source?.toExtractorLink(
this, this,
nameTransformer(subList.second), 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")
} }
} }
} }

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)
} }