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
version = 8
version = 9
cloudstream {

View File

@ -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
)
}
} else {
tryParseJson(responseJson)
}
suspend fun getKey(): String? {
data class KeyObject(
@JsonProperty("key") val key: String? = null
)
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
) {
app.baseClient.newWebSocket(
requestCreator("GET", socketUrl),
object : WebSocketListener() {
val sidRegex = Regex("""sid.*"(.*?)"""")
val sourceRegex = Regex("""\{.*\}""")
val codeRegex = Regex("""^\d*""")
/**
* 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
}
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) {
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")
}
}
}
}
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
) {
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,89 +724,103 @@ 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 sourceObject = sourceResource.value
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"
}
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"
)
)
list.forEach { subList ->
subList.first?.forEach { source ->
source?.toExtractorLink(
this,
nameTransformer(subList.second),
)?.forEach(callback)
}
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 ->
subtitleCallback.invoke(subtitleFile)
}
isDone = true
}
var elapsedTime = 0
val maxTime = 30
val list = listOf(
sourceObject.sources to "source 1",
sourceObject.sources1 to "source 2",
sourceObject.sources2 to "source 3",
sourceObject.sourcesBackup to "source backup"
)
while (elapsedTime < maxTime && !isDone) {
elapsedTime++
delay(1_000)
list.forEach { subList ->
subList.first?.forEach { source ->
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 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)
}