forked from recloudstream/cloudstream
fix zoro
This commit is contained in:
parent
3be125f12a
commit
ed573f4f22
5 changed files with 293 additions and 203 deletions
|
@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
||||||
import com.lagradost.cloudstream3.movieproviders.*
|
import com.lagradost.cloudstream3.movieproviders.*
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import okhttp3.Interceptor
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
@ -394,6 +395,11 @@ abstract class MainAPI {
|
||||||
): Boolean {
|
): Boolean {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An okhttp interceptor for used in OkHttpDataSource */
|
||||||
|
open fun getVideoInterceptor(extractorLink: ExtractorLink) : Interceptor? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Might need a different implementation for desktop*/
|
/** Might need a different implementation for desktop*/
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
package com.lagradost.cloudstream3.animeproviders
|
package com.lagradost.cloudstream3.animeproviders
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.util.NameTransformer
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
|
||||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider
|
|
||||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.extractRabbitStream
|
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.extractRabbitStream
|
||||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.toExtractorLink
|
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.runSflixExtractorVerifierJob
|
||||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.toSubtitleFile
|
import com.lagradost.cloudstream3.network.Requests.Companion.await
|
||||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import okhttp3.Interceptor
|
||||||
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
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
private const val OPTIONS = "OPTIONS"
|
||||||
|
|
||||||
class ZoroProvider : MainAPI() {
|
class ZoroProvider : MainAPI() {
|
||||||
override var mainUrl = "https://zoro.to"
|
override var mainUrl = "https://zoro.to"
|
||||||
override var name = "Zoro"
|
override var name = "Zoro"
|
||||||
|
@ -285,26 +286,48 @@ class ZoroProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getM3u8FromRapidCloud(url: String): String {
|
|
||||||
return /*Regex("""/(embed-\d+)/(.*?)\?z=""").find(url)?.groupValues?.let {
|
|
||||||
val jsonLink = "https://rapid-cloud.ru/ajax/${it[1]}/getSources?id=${it[2]}"
|
|
||||||
app.get(jsonLink).text
|
|
||||||
} ?:*/ app.get(
|
|
||||||
"$url&autoPlay=1&oa=0",
|
|
||||||
headers = mapOf(
|
|
||||||
"Referer" to "https://zoro.to/",
|
|
||||||
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0"
|
|
||||||
),
|
|
||||||
interceptor = WebViewResolver(
|
|
||||||
Regex("""/getSources""")
|
|
||||||
)
|
|
||||||
).text
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class RapidCloudResponse(
|
private data class RapidCloudResponse(
|
||||||
@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 */
|
||||||
|
var sid: HashMap<Int, String?> = hashMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an identical Options request before .ts request
|
||||||
|
* Adds an SID header to the .ts request.
|
||||||
|
* */
|
||||||
|
override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor {
|
||||||
|
return Interceptor { chain ->
|
||||||
|
val request = chain.request()
|
||||||
|
if (request.url.toString().endsWith(".ts")
|
||||||
|
&& request.method != OPTIONS
|
||||||
|
// No option requests on VidCloud
|
||||||
|
&& !request.url.toString().contains("betterstream")
|
||||||
|
) {
|
||||||
|
val newRequest =
|
||||||
|
chain.request()
|
||||||
|
.newBuilder().apply {
|
||||||
|
sid[extractorLink.url.hashCode()]?.let { sid ->
|
||||||
|
addHeader("SID", sid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
val options = request.newBuilder().method(OPTIONS, request.body).build()
|
||||||
|
ioSafe { app.baseClient.newCall(options).await() }
|
||||||
|
|
||||||
|
return@Interceptor chain.proceed(newRequest)
|
||||||
|
} else {
|
||||||
|
return@Interceptor chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun loadLinks(
|
override suspend fun loadLinks(
|
||||||
data: String,
|
data: String,
|
||||||
isCasting: Boolean,
|
isCasting: Boolean,
|
||||||
|
@ -322,6 +345,8 @@ class ZoroProvider : MainAPI() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val extractorData =
|
||||||
|
"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 {
|
||||||
|
@ -330,11 +355,18 @@ class ZoroProvider : MainAPI() {
|
||||||
val extractorLink = app.get(
|
val extractorLink = app.get(
|
||||||
link,
|
link,
|
||||||
).mapped<RapidCloudResponse>().link
|
).mapped<RapidCloudResponse>().link
|
||||||
val hasLoadedExtractorLink = loadExtractor(extractorLink, mainUrl, callback)
|
val hasLoadedExtractorLink =
|
||||||
|
loadExtractor(extractorLink, "https://rapid-cloud.ru/", callback)
|
||||||
|
|
||||||
if (!hasLoadedExtractorLink) {
|
if (!hasLoadedExtractorLink) {
|
||||||
extractRabbitStream(extractorLink, subtitleCallback, callback) { sourceName ->
|
extractRabbitStream(
|
||||||
sourceName + " - ${it.first}"
|
extractorLink,
|
||||||
|
subtitleCallback,
|
||||||
|
// Blacklist VidCloud for now
|
||||||
|
{ videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) },
|
||||||
|
extractorData
|
||||||
|
) { sourceName ->
|
||||||
|
sourceName + " - ${it.first}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,13 @@ 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.setDuration
|
import com.lagradost.cloudstream3.LoadResponse.Companion.setDuration
|
||||||
|
import com.lagradost.cloudstream3.animeproviders.ZoroProvider
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.network.AppResponse
|
import com.lagradost.cloudstream3.network.AppResponse
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
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.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
|
||||||
|
@ -357,182 +359,24 @@ open class SflixProvider : MainAPI() {
|
||||||
return !urls.isNullOrEmpty()
|
return !urls.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PollingData(
|
|
||||||
@JsonProperty("sid") val sid: String? = null,
|
|
||||||
@JsonProperty("upgrades") val upgrades: ArrayList<String> = arrayListOf(),
|
|
||||||
@JsonProperty("pingInterval") val pingInterval: Int? = null,
|
|
||||||
@JsonProperty("pingTimeout") val pingTimeout: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
# python code to figure out the time offset based on code if necessary
|
|
||||||
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
|
|
||||||
code = "Nxa_-bM"
|
|
||||||
total = 0
|
|
||||||
for i, char in enumerate(code[::-1]):
|
|
||||||
index = chars.index(char)
|
|
||||||
value = index * 64**i
|
|
||||||
total += value
|
|
||||||
print(f"total {total}")
|
|
||||||
*/
|
|
||||||
private fun generateTimeStamp(): String {
|
|
||||||
val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
|
|
||||||
var code = ""
|
|
||||||
var time = unixTimeMS
|
|
||||||
while (time > 0) {
|
|
||||||
code += chars[(time % (chars.length)).toInt()]
|
|
||||||
time /= chars.length
|
|
||||||
}
|
|
||||||
return code.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a session
|
|
||||||
* */
|
|
||||||
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: AppResponse,
|
|
||||||
data: PollingData,
|
|
||||||
baseUrl: String
|
|
||||||
): Pair<PollingData, Boolean> {
|
|
||||||
if (!response.response.isSuccessful) {
|
|
||||||
return negotiateNewSid(baseUrl)?.let {
|
|
||||||
it to true
|
|
||||||
} ?: data to false
|
|
||||||
}
|
|
||||||
return data to false
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun extractorVerifierJob(extractorData: String?) {
|
override suspend fun extractorVerifierJob(extractorData: String?) {
|
||||||
if (extractorData == null) return
|
runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
|
||||||
|
|
||||||
val headers = mapOf(
|
|
||||||
"Referer" to "https://rabbitstream.net/"
|
|
||||||
)
|
|
||||||
|
|
||||||
var data = negotiateNewSid(extractorData) ?: return
|
|
||||||
// 40 is hardcoded, dunno how it's generated, but it seems to work everywhere.
|
|
||||||
// This request is obligatory
|
|
||||||
app.post(
|
|
||||||
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
|
|
||||||
data = 40, headers = headers
|
|
||||||
)//.also { println("First post ${it.text}") }
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// 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 -> authInt
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}"
|
|
||||||
|
|
||||||
getUpdatedData(
|
|
||||||
app.post(url, data = authData, headers = headers),
|
|
||||||
data,
|
|
||||||
extractorData
|
|
||||||
).also {
|
|
||||||
newAuth = it.second
|
|
||||||
data = it.first
|
|
||||||
}
|
|
||||||
|
|
||||||
//.also { println("Sflix post job ${it.text}") }
|
|
||||||
Log.d(this.name, "Running ${this.name} job $url")
|
|
||||||
|
|
||||||
val time = measureTimeMillis {
|
|
||||||
// This acts as a timeout
|
|
||||||
val getResponse = app.get(
|
|
||||||
"${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}",
|
|
||||||
timeout = 60,
|
|
||||||
headers = headers
|
|
||||||
)
|
|
||||||
// .also { println("Sflix get job ${it.text}") }
|
|
||||||
if (getResponse.text.contains("sid")) {
|
|
||||||
reconnect = true
|
|
||||||
// println("Reconnecting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Always waits even if the get response is instant, to prevent a while true loop.
|
|
||||||
if (time < interval - 4000)
|
|
||||||
delay(4000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element.toSearchResult(): SearchResponse {
|
private fun Element.toSearchResult(): SearchResponse {
|
||||||
val inner = this.selectFirst("div.film-poster")
|
val img = this.select("img")
|
||||||
val img = inner.select("img")
|
|
||||||
val title = img.attr("title")
|
val title = img.attr("title")
|
||||||
val posterUrl = img.attr("data-src") ?: img.attr("src")
|
val posterUrl = img.attr("data-src")
|
||||||
val href = fixUrl(inner.select("a").attr("href"))
|
val href = fixUrl(this.select("a").attr("href"))
|
||||||
val isMovie = href.contains("/movie/")
|
val isMovie = href.contains("/movie/")
|
||||||
val otherInfo = this.selectFirst("div.film-detail > div.fd-infor")?.select("span")?.toList() ?: listOf()
|
|
||||||
var rating: Int? = null
|
|
||||||
var year: Int? = null
|
|
||||||
var quality: SearchQuality? = null
|
|
||||||
when (otherInfo.size) {
|
|
||||||
1 -> {
|
|
||||||
year = otherInfo[0]?.text()?.trim()?.toIntOrNull()
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
year = otherInfo[0]?.text()?.trim()?.toIntOrNull()
|
|
||||||
}
|
|
||||||
3 -> {
|
|
||||||
rating = otherInfo[0]?.text()?.toRatingInt()
|
|
||||||
quality = getQualityFromString(otherInfo[1]?.text())
|
|
||||||
year = otherInfo[2]?.text()?.trim()?.toIntOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (isMovie) {
|
return if (isMovie) {
|
||||||
MovieSearchResponse(
|
MovieSearchResponse(
|
||||||
title,
|
title,
|
||||||
href,
|
href,
|
||||||
this@SflixProvider.name,
|
this@SflixProvider.name,
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
posterUrl = posterUrl,
|
posterUrl,
|
||||||
year = year,
|
null
|
||||||
quality = quality
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TvSeriesSearchResponse(
|
TvSeriesSearchResponse(
|
||||||
|
@ -541,14 +385,179 @@ open class SflixProvider : MainAPI() {
|
||||||
this@SflixProvider.name,
|
this@SflixProvider.name,
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
posterUrl,
|
posterUrl,
|
||||||
year = year,
|
null,
|
||||||
episodes = null,
|
null
|
||||||
quality = quality
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
data class PollingData(
|
||||||
|
@JsonProperty("sid") val sid: String? = null,
|
||||||
|
@JsonProperty("upgrades") val upgrades: ArrayList<String> = arrayListOf(),
|
||||||
|
@JsonProperty("pingInterval") val pingInterval: Int? = null,
|
||||||
|
@JsonProperty("pingTimeout") val pingTimeout: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
# python code to figure out the time offset based on code if necessary
|
||||||
|
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
|
||||||
|
code = "Nxa_-bM"
|
||||||
|
total = 0
|
||||||
|
for i, char in enumerate(code[::-1]):
|
||||||
|
index = chars.index(char)
|
||||||
|
value = index * 64**i
|
||||||
|
total += value
|
||||||
|
print(f"total {total}")
|
||||||
|
*/
|
||||||
|
private fun generateTimeStamp(): String {
|
||||||
|
val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
|
||||||
|
var code = ""
|
||||||
|
var time = unixTimeMS
|
||||||
|
while (time > 0) {
|
||||||
|
code += chars[(time % (chars.length)).toInt()]
|
||||||
|
time /= chars.length
|
||||||
|
}
|
||||||
|
return code.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: AppResponse,
|
||||||
|
data: PollingData,
|
||||||
|
baseUrl: String
|
||||||
|
): Pair<PollingData, Boolean> {
|
||||||
|
if (!response.response.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}",
|
||||||
|
data = 40, 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, data = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun String?.isValidServer(): Boolean {
|
fun String?.isValidServer(): Boolean {
|
||||||
if (this.isNullOrEmpty()) return false
|
if (this.isNullOrEmpty()) return false
|
||||||
if (this.equals("UpCloud", ignoreCase = true) || this.equals(
|
if (this.equals("UpCloud", ignoreCase = true) || this.equals(
|
||||||
|
@ -563,7 +572,7 @@ open class SflixProvider : MainAPI() {
|
||||||
fun Sources.toExtractorLink(
|
fun Sources.toExtractorLink(
|
||||||
caller: MainAPI,
|
caller: MainAPI,
|
||||||
name: String,
|
name: String,
|
||||||
extractorData: String? = null
|
extractorData: String? = null,
|
||||||
): List<ExtractorLink>? {
|
): List<ExtractorLink>? {
|
||||||
return this.file?.let { file ->
|
return this.file?.let { file ->
|
||||||
//println("FILE::: $file")
|
//println("FILE::: $file")
|
||||||
|
@ -632,13 +641,30 @@ open class SflixProvider : MainAPI() {
|
||||||
val number =
|
val number =
|
||||||
Regex("""recaptchaNumber = '(.*?)'""").find(iframe.text)?.groupValues?.get(1)
|
Regex("""recaptchaNumber = '(.*?)'""").find(iframe.text)?.groupValues?.get(1)
|
||||||
|
|
||||||
|
var sid: String? = null
|
||||||
|
|
||||||
|
extractorData?.let { negotiateNewSid(it) }?.also {
|
||||||
|
app.post(
|
||||||
|
"$extractorData&t=${generateTimeStamp()}&sid=${it.sid}",
|
||||||
|
data = "40",
|
||||||
|
timeout = 60
|
||||||
|
)
|
||||||
|
val text = app.get(
|
||||||
|
"$extractorData&t=${generateTimeStamp()}&sid=${it.sid}",
|
||||||
|
timeout = 60
|
||||||
|
).text.replaceBefore("{", "")
|
||||||
|
|
||||||
|
sid = parseJson<PollingData>(text).sid
|
||||||
|
ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${it.sid}") }
|
||||||
|
}
|
||||||
|
|
||||||
val mapped = app.get(
|
val mapped = app.get(
|
||||||
"${
|
"${
|
||||||
mainIframeUrl.replace(
|
mainIframeUrl.replace(
|
||||||
"/embed",
|
"/embed",
|
||||||
"/ajax/embed"
|
"/ajax/embed"
|
||||||
)
|
)
|
||||||
}/getSources?id=$mainIframeId&_token=$iframeToken&_number=$number",
|
}/getSources?id=$mainIframeId&_token=$iframeToken&_number=$number${sid?.let { "&sid=$it" } ?: ""}",
|
||||||
referer = mainUrl,
|
referer = mainUrl,
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"X-Requested-With" to "XMLHttpRequest",
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
|
@ -667,11 +693,18 @@ open class SflixProvider : MainAPI() {
|
||||||
mapped.sources2 to "source 3",
|
mapped.sources2 to "source 3",
|
||||||
mapped.sourcesBackup to "source backup"
|
mapped.sourcesBackup to "source backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
list.forEach { subList ->
|
list.forEach { subList ->
|
||||||
subList.first?.forEach { source ->
|
subList.first?.forEach { source ->
|
||||||
source?.toExtractorLink(this, nameTransformer(subList.second), extractorData)
|
source?.toExtractorLink(
|
||||||
?.forEach(callback)
|
this,
|
||||||
|
nameTransformer(subList.second),
|
||||||
|
extractorData,
|
||||||
|
)
|
||||||
|
?.forEach {
|
||||||
|
// Sets Zoro SID used for video loading
|
||||||
|
(this as? ZoroProvider)?.sid?.set(it.url.hashCode(), sid)
|
||||||
|
callback(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class APIRepository(val api: MainAPI) {
|
||||||
override val supportedTypes = emptySet<TvType>()
|
override val supportedTypes = emptySet<TvType>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInvalidData(data : String): Boolean {
|
fun isInvalidData(data: String): Boolean {
|
||||||
return data.isEmpty() || data == "[]" || data == "about:blank"
|
return data.isEmpty() || data == "[]" || data == "about:blank"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ class APIRepository(val api: MainAPI) {
|
||||||
val hasQuickSearch = api.hasQuickSearch
|
val hasQuickSearch = api.hasQuickSearch
|
||||||
|
|
||||||
suspend fun load(url: String): Resource<LoadResponse> {
|
suspend fun load(url: String): Resource<LoadResponse> {
|
||||||
if(isInvalidData(url)) throw ErrorLoadingException()
|
if (isInvalidData(url)) throw ErrorLoadingException()
|
||||||
|
|
||||||
return safeApiCall {
|
return safeApiCall {
|
||||||
api.load(api.fixUrl(url)) ?: throw ErrorLoadingException()
|
api.load(api.fixUrl(url)) ?: throw ErrorLoadingException()
|
||||||
|
@ -64,6 +64,12 @@ class APIRepository(val api: MainAPI) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun extractorVerifierJob(extractorData: String?) {
|
||||||
|
safeApiCall {
|
||||||
|
api.extractorVerifierJob(extractorData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun loadLinks(
|
suspend fun loadLinks(
|
||||||
data: String,
|
data: String,
|
||||||
isCasting: Boolean,
|
isCasting: Boolean,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.util.Log
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
||||||
|
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||||
import com.google.android.exoplayer2.source.MergingMediaSource
|
import com.google.android.exoplayer2.source.MergingMediaSource
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource
|
import com.google.android.exoplayer2.source.SingleSampleMediaSource
|
||||||
|
@ -17,12 +18,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelector
|
||||||
import com.google.android.exoplayer2.ui.SubtitleView
|
import com.google.android.exoplayer2.ui.SubtitleView
|
||||||
import com.google.android.exoplayer2.upstream.DataSource
|
import com.google.android.exoplayer2.upstream.DataSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
import com.google.android.exoplayer2.util.MimeTypes
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.getApiFromName
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
@ -307,8 +309,19 @@ class CS3IPlayer : IPlayer {
|
||||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||||
|
|
||||||
private fun createOnlineSource(link: ExtractorLink): DataSource.Factory {
|
private fun createOnlineSource(link: ExtractorLink): DataSource.Factory {
|
||||||
// Because Trailers.to seems to fail with http/1.1 the normal one uses.
|
val provider = getApiFromName(link.source)
|
||||||
return DefaultHttpDataSource.Factory().apply {
|
val interceptor = provider.getVideoInterceptor(link)
|
||||||
|
|
||||||
|
val client = app.baseClient
|
||||||
|
.let {
|
||||||
|
if (interceptor != null)
|
||||||
|
it.newBuilder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.build()
|
||||||
|
else it
|
||||||
|
}
|
||||||
|
|
||||||
|
return OkHttpDataSource.Factory(client).apply {
|
||||||
setUserAgent(USER_AGENT)
|
setUserAgent(USER_AGENT)
|
||||||
val headers = mapOf(
|
val headers = mapOf(
|
||||||
"referer" to link.referer,
|
"referer" to link.referer,
|
||||||
|
@ -322,7 +335,7 @@ class CS3IPlayer : IPlayer {
|
||||||
setDefaultRequestProperties(headers)
|
setDefaultRequestProperties(headers)
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/69040127/error-code-io-bad-http-status-exoplayer-android
|
//https://stackoverflow.com/questions/69040127/error-code-io-bad-http-status-exoplayer-android
|
||||||
setAllowCrossProtocolRedirects(true)
|
// setAllowCrossProtocolRedirects(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue