From ed573f4f223f70ff2d7308ff60f3ef4a527c1e3b Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:50:16 +0200 Subject: [PATCH] fix zoro --- .../com/lagradost/cloudstream3/MainAPI.kt | 6 + .../animeproviders/ZoroProvider.kt | 82 ++-- .../movieproviders/SflixProvider.kt | 377 ++++++++++-------- .../cloudstream3/ui/APIRepository.kt | 10 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 21 +- 5 files changed, 293 insertions(+), 203 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 5e075016..3b27e9c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider import com.lagradost.cloudstream3.movieproviders.* import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.ExtractorLink +import okhttp3.Interceptor import java.util.* import kotlin.math.absoluteValue @@ -394,6 +395,11 @@ abstract class MainAPI { ): Boolean { throw NotImplementedError() } + + /** An okhttp interceptor for used in OkHttpDataSource */ + open fun getVideoInterceptor(extractorLink: ExtractorLink) : Interceptor? { + return null + } } /** Might need a different implementation for desktop*/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt index ed8027ff..aa3b3186 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ZoroProvider.kt @@ -1,22 +1,23 @@ package com.lagradost.cloudstream3.animeproviders +import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.util.NameTransformer import com.fasterxml.jackson.module.kotlin.readValue 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.toExtractorLink -import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.toSubtitleFile -import com.lagradost.cloudstream3.network.WebViewResolver +import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.runSflixExtractorVerifierJob +import com.lagradost.cloudstream3.network.Requests.Companion.await +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.loadExtractor +import okhttp3.Interceptor import org.jsoup.Jsoup import org.jsoup.nodes.Element import java.net.URI import java.util.* +private const val OPTIONS = "OPTIONS" + class ZoroProvider : MainAPI() { override var mainUrl = "https://zoro.to" 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( @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 = 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( data: String, isCasting: Boolean, @@ -322,6 +345,8 @@ class ZoroProvider : MainAPI() { ) } + val extractorData = + "https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling" // Prevent duplicates servers.distinctBy { it.second }.apmap { @@ -330,11 +355,18 @@ class ZoroProvider : MainAPI() { val extractorLink = app.get( link, ).mapped().link - val hasLoadedExtractorLink = loadExtractor(extractorLink, mainUrl, callback) + val hasLoadedExtractorLink = + loadExtractor(extractorLink, "https://rapid-cloud.ru/", callback) if (!hasLoadedExtractorLink) { - extractRabbitStream(extractorLink, subtitleCallback, callback) { sourceName -> - sourceName + " - ${it.first}" + extractRabbitStream( + extractorLink, + subtitleCallback, + // Blacklist VidCloud for now + { videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) }, + extractorData + ) { sourceName -> + sourceName + " - ${it.first}" } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt index b8264606..b6338358 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt @@ -7,11 +7,13 @@ 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.setDuration +import com.lagradost.cloudstream3.animeproviders.ZoroProvider import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.network.AppResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson 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 @@ -357,182 +359,24 @@ open class SflixProvider : MainAPI() { return !urls.isNullOrEmpty() } - data class PollingData( - @JsonProperty("sid") val sid: String? = null, - @JsonProperty("upgrades") val upgrades: ArrayList = 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(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 { - if (!response.response.isSuccessful) { - return negotiateNewSid(baseUrl)?.let { - it to true - } ?: data to false - } - return data to false - } - override suspend fun extractorVerifierJob(extractorData: String?) { - if (extractorData == null) return - - 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( - 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) - } + runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/") } private fun Element.toSearchResult(): SearchResponse { - val inner = this.selectFirst("div.film-poster") - val img = inner.select("img") + val img = this.select("img") val title = img.attr("title") - val posterUrl = img.attr("data-src") ?: img.attr("src") - val href = fixUrl(inner.select("a").attr("href")) + val posterUrl = img.attr("data-src") + val href = fixUrl(this.select("a").attr("href")) 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) { MovieSearchResponse( title, href, this@SflixProvider.name, TvType.Movie, - posterUrl = posterUrl, - year = year, - quality = quality + posterUrl, + null ) } else { TvSeriesSearchResponse( @@ -541,14 +385,179 @@ open class SflixProvider : MainAPI() { this@SflixProvider.name, TvType.Movie, posterUrl, - year = year, - episodes = null, - quality = quality + null, + null ) } } companion object { + data class PollingData( + @JsonProperty("sid") val sid: String? = null, + @JsonProperty("upgrades") val upgrades: ArrayList = 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(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 { + 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 { + 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( + 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 { if (this.isNullOrEmpty()) return false if (this.equals("UpCloud", ignoreCase = true) || this.equals( @@ -563,7 +572,7 @@ open class SflixProvider : MainAPI() { fun Sources.toExtractorLink( caller: MainAPI, name: String, - extractorData: String? = null + extractorData: String? = null, ): List? { return this.file?.let { file -> //println("FILE::: $file") @@ -632,13 +641,30 @@ open class SflixProvider : MainAPI() { val number = 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(text).sid + ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${it.sid}") } + } + val mapped = app.get( "${ mainIframeUrl.replace( "/embed", "/ajax/embed" ) - }/getSources?id=$mainIframeId&_token=$iframeToken&_number=$number", + }/getSources?id=$mainIframeId&_token=$iframeToken&_number=$number${sid?.let { "&sid=$it" } ?: ""}", referer = mainUrl, headers = mapOf( "X-Requested-With" to "XMLHttpRequest", @@ -667,11 +693,18 @@ open class SflixProvider : MainAPI() { mapped.sources2 to "source 3", mapped.sourcesBackup to "source backup" ) - list.forEach { subList -> subList.first?.forEach { source -> - source?.toExtractorLink(this, nameTransformer(subList.second), extractorData) - ?.forEach(callback) + 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) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 8cdd09aa..1dd65ae2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -19,7 +19,7 @@ class APIRepository(val api: MainAPI) { override val supportedTypes = emptySet() } - fun isInvalidData(data : String): Boolean { + fun isInvalidData(data: String): Boolean { return data.isEmpty() || data == "[]" || data == "about:blank" } } @@ -30,7 +30,7 @@ class APIRepository(val api: MainAPI) { val hasQuickSearch = api.hasQuickSearch suspend fun load(url: String): Resource { - if(isInvalidData(url)) throw ErrorLoadingException() + if (isInvalidData(url)) throw ErrorLoadingException() return safeApiCall { 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( data: String, isCasting: Boolean, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index dcfb6a0e..85b660ca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -8,6 +8,7 @@ import android.util.Log import android.widget.FrameLayout import com.google.android.exoplayer2.* 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.MergingMediaSource 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.upstream.DataSource 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.LeastRecentlyUsedCacheEvictor import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.util.MimeTypes +import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.ExtractorLink @@ -307,8 +309,19 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null private fun createOnlineSource(link: ExtractorLink): DataSource.Factory { - // Because Trailers.to seems to fail with http/1.1 the normal one uses. - return DefaultHttpDataSource.Factory().apply { + val provider = getApiFromName(link.source) + 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) val headers = mapOf( "referer" to link.referer, @@ -322,7 +335,7 @@ class CS3IPlayer : IPlayer { setDefaultRequestProperties(headers) //https://stackoverflow.com/questions/69040127/error-code-io-bad-http-status-exoplayer-android - setAllowCrossProtocolRedirects(true) +// setAllowCrossProtocolRedirects(true) } }