Fix SflixProvider

This commit is contained in:
Blatzar 2022-09-30 15:56:16 +02:00
parent 3fdb1849d5
commit 2bc61ed11a
4 changed files with 159 additions and 268 deletions

View File

@ -1,12 +1,12 @@
// use an integer for version numbers // use an integer for version numbers
version = 7 version = 8
cloudstream { cloudstream {
language = "en" language = "en"
// All of these properties are optional, you can safely remove them // All of these properties are optional, you can safely remove them
description = "Also includes Dopebox, Solarmovie, Zoro and 2embed" description = "Also includes Dopebox, Solarmovie, Zoro, HDToday and 2embed"
// authors = listOf("Cloudburst") // authors = listOf("Cloudburst")
/** /**
@ -20,6 +20,8 @@ cloudstream {
tvTypes = listOf( tvTypes = listOf(
"TvSeries", "TvSeries",
"Movie", "Movie",
"Anime",
"AnimeMovie",
) )
iconUrl = "https://www.google.com/s2/favicons?domain=www.2embed.to&sz=%size%" iconUrl = "https://www.google.com/s2/favicons?domain=www.2embed.to&sz=%size%"

View File

@ -3,23 +3,22 @@ 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.animeproviders.ZoroProvider import com.lagradost.cloudstream3.mvvm.Resource
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.NiceResponse import com.lagradost.nicehttp.requestCreator
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.WebSocket
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
@ -29,7 +28,6 @@ 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"
@ -355,18 +353,15 @@ open class SflixProvider : MainAPI() {
?: return@suspendSafeApiCall ?: return@suspendSafeApiCall
// Some smarter ws11 or w10 selection might be required in the future. // Some smarter ws11 or w10 selection might be required in the future.
val extractorData = // val extractorData =
"https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling" // "https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling"
if (iframeLink.contains("streamlare", ignoreCase = true)) { val hasLoadedExtractor = loadExtractor(iframeLink, null, subtitleCallback, callback)
loadExtractor(iframeLink, null, subtitleCallback, callback) if (!hasLoadedExtractor) {
} else {
extractRabbitStream( extractRabbitStream(
iframeLink, iframeLink,
subtitleCallback, subtitleCallback,
callback, callback,
false,
decryptKey = getKey()
) { it } ) { it }
} }
} }
@ -375,10 +370,6 @@ 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")
@ -459,149 +450,69 @@ open class SflixProvider : MainAPI() {
return code.reversed() return code.reversed()
} }
suspend fun getKey(): String? { fun getSourceObject(responseJson: String?, decryptKey: String?): SourceObject? {
data class KeyObject( if (responseJson == null) return null
@JsonProperty("key") val key: String? = null return if (decryptKey != null) {
) val encryptedMap = tryParseJson<SourceObjectEncrypted>(responseJson)
return app.get("https://raw.githubusercontent.com/BlipBlob/blabflow/main/keys.json") val sources = encryptedMap?.sources
.parsed<KeyObject>().key
}
/** if (sources == null || encryptedMap.encrypted == false) {
* Generates a session tryParseJson(responseJson)
* 1 Get request. } else {
* */ val decrypted = decryptMapped<List<Sources>>(sources, decryptKey)
private suspend fun negotiateNewSid(baseUrl: String): PollingData? { SourceObject(
// Tries multiple times sources = decrypted,
for (i in 1..5) { tracks = encryptedMap.tracks
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("{", "") } else {
).sid tryParseJson(responseJson)
}
// 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( private fun getSources(
api: MainAPI, socketUrl: String,
extractorData: String?, id: String,
referer: String callback: suspend (Resource<SourceObject>) -> Unit
) { ) {
if (extractorData == null) return app.baseClient.newWebSocket(
val headers = mapOf( requestCreator("GET", socketUrl),
"Referer" to referer // "https://rabbitstream.net/" object : WebSocketListener() {
) val sidRegex = Regex("""sid.*"(.*?)"""")
val sourceRegex = Regex("""\{.*\}""")
val codeRegex = Regex("""^\d*""")
lateinit var data: PollingData var key: String? = null
var reconnectSid = ""
initPolling(extractorData, referer) override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
.also { ioSafe {
data = it.first ?: throw RuntimeException("Data Null") callback(Resource.Failure(false, code, null, reason))
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}" override fun onMessage(webSocket: WebSocket, text: String) {
Log.d("getSources", "onMessage $text")
val code = codeRegex.find(text)?.value?.toIntOrNull() ?: return
getUpdatedData( when (code) {
app.post(url, json = authData, headers = headers), 0 -> webSocket.send("40")
data, 40 -> {
extractorData key = sidRegex.find(text)?.groupValues?.get(1)
).also { webSocket.send("""42["getSources",{"id":"$id"}]""")
newAuth = it.second }
data = it.first 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")
}
}
}
} }
)
//.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
@ -611,7 +522,8 @@ open class SflixProvider : MainAPI() {
} }
// For re-use in Zoro // For re-use in Zoro
private suspend fun Sources.toExtractorLink( private suspend
fun Sources.toExtractorLink(
caller: MainAPI, caller: MainAPI,
name: String, name: String,
extractorData: String? = null, extractorData: String? = null,
@ -693,7 +605,10 @@ open class SflixProvider : MainAPI() {
return currentKey return currentKey
} }
private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String { private fun decryptSourceUrl(
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")
@ -709,7 +624,8 @@ open class SflixProvider : MainAPI() {
return String(decryptedData, StandardCharsets.UTF_8) 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)) return tryParseJson(decrypt(input, key))
} }
@ -726,104 +642,89 @@ 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 sid: String? = null var isDone = false
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 = parseJson<PollingData>(text).sid // Hardcoded for now, does not support Zoro yet.
ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}") } getSources(
"wss://wsx.dokicloud.one/socket.io/?EIO=4&transport=websocket",
mainIframeId
) { sourceResource ->
if (sourceResource !is Resource.Success) {
isDone = true
return@getSources
} }
}
val getSourcesUrl = "${ val sourceObject = sourceResource.value
mainIframeUrl.replace(
"/embed", sourceObject.tracks?.forEach { track ->
"/ajax/embed" 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"
) )
}/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") list.forEach { subList ->
val sourceObject = if (response.text.contains("encrypted") && 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
} }
val list = listOf( var elapsedTime = 0
sourceObject.sources to "source 1", val maxTime = 30
sourceObject.sources1 to "source 2",
sourceObject.sources2 to "source 3",
sourceObject.sourcesBackup to "source backup"
)
list.forEach { subList -> while (elapsedTime < maxTime && !isDone) {
subList.first?.forEach { source -> elapsedTime++
source?.toExtractorLink( delay(1_000)
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

@ -1,9 +1,7 @@
package com.lagradost package com.lagradost
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.SflixProvider.Companion.extractRabbitStream import com.lagradost.SflixProvider.Companion.extractRabbitStream
import com.lagradost.SflixProvider.Companion.runSflixExtractorVerifierJob
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
@ -64,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, false, decryptKey = SflixProvider.getKey()) { it } extractRabbitStream(iframeLink, subtitleCallback, callback) { it }
} else { } else {
loadExtractor(iframeLink, embedUrl, subtitleCallback, callback) loadExtractor(iframeLink, embedUrl, subtitleCallback, callback)
} }
@ -72,8 +70,8 @@ class TwoEmbedProvider : TmdbProvider() {
return true return true
} }
override suspend fun extractorVerifierJob(extractorData: String?) { // override suspend fun extractorVerifierJob(extractorData: String?) {
Log.d(this.name, "Starting ${this.name} job!") // Log.d(this.name, "Starting ${this.name} job!")
runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/") // runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
} // }
} }

View File

@ -1,9 +1,6 @@
package com.lagradost package com.lagradost
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.SflixProvider.Companion.extractRabbitStream
import com.lagradost.SflixProvider.Companion.runSflixExtractorVerifierJob
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
@ -281,11 +278,6 @@ class ZoroProvider : MainAPI() {
@JsonProperty("link") val link: String @JsonProperty("link") val link: String
) )
override suspend fun extractorVerifierJob(extractorData: String?) {
Log.d(this.name, "Starting ${this.name} job!")
runSflixExtractorVerifierJob(this, extractorData, "https://rapid-cloud.ru/")
}
/** Url hashcode to sid */ /** Url hashcode to sid */
var sid: HashMap<Int, String?> = hashMapOf() var sid: HashMap<Int, String?> = hashMapOf()
@ -343,8 +335,8 @@ class ZoroProvider : MainAPI() {
) )
} }
val extractorData = // val extractorData =
"https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling" // "https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling"
// Prevent duplicates // Prevent duplicates
servers.distinctBy { it.second }.apmap { servers.distinctBy { it.second }.apmap {
@ -354,21 +346,19 @@ class ZoroProvider : MainAPI() {
link, link,
).parsed<RapidCloudResponse>().link ).parsed<RapidCloudResponse>().link
// val hasLoadedExtractorLink = // val hasLoadedExtractorLink =
// loadExtractor(extractorLink, "https://rapid-cloud.ru/", subtitleCallback, callback) loadExtractor(extractorLink, "https://rapid-cloud.ru/", subtitleCallback, callback)
// if (!hasLoadedExtractorLink) { // if (!hasLoadedExtractorLink) {
extractRabbitStream( // extractRabbitStream(
extractorLink, // extractorLink,
subtitleCallback, // subtitleCallback,
// Blacklist VidCloud for now // // Blacklist VidCloud for now
{ videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) }, // { videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) },
false, //// false,
// extractorData, //// extractorData,
decryptKey = getKey() //// decryptKey = getKey()
// ) { sourceName ->
) { sourceName -> // sourceName + " - ${it.first}"
sourceName + " - ${it.first}" // }
}
// } // }
} }