AquaStream/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt

574 lines
22 KiB
Kotlin
Raw Normal View History

2021-10-05 16:39:38 +00:00
package com.lagradost.cloudstream3.movieproviders
2022-02-11 09:17:04 +00:00
import android.util.Log
2021-10-05 16:39:38 +00:00
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
2022-02-11 09:17:04 +00:00
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
2022-02-05 23:58:56 +00:00
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.setDuration
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
2022-02-14 12:50:05 +00:00
import com.lagradost.cloudstream3.network.AppResponse
2021-10-05 16:39:38 +00:00
import com.lagradost.cloudstream3.network.WebViewResolver
2022-02-11 09:17:04 +00:00
import com.lagradost.cloudstream3.network.getRequestCreator
import com.lagradost.cloudstream3.network.text
2022-01-16 22:31:42 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
2021-10-05 16:39:38 +00:00
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
2021-10-05 16:39:38 +00:00
import com.lagradost.cloudstream3.utils.getQualityFromName
2022-02-11 09:17:04 +00:00
import kotlinx.coroutines.delay
2021-10-05 16:39:38 +00:00
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.net.URI
2022-02-11 09:17:04 +00:00
import kotlin.system.measureTimeMillis
2021-10-05 16:39:38 +00:00
class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
override val mainUrl = providerUrl
override val name = providerName
2021-10-05 16:39:38 +00:00
override val hasQuickSearch = false
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val usesWebView = true
override val supportedTypes = setOf(
2021-12-13 18:41:33 +00:00
TvType.Movie,
TvType.TvSeries,
)
2022-01-14 18:14:24 +00:00
override val vpnStatus = VPNStatus.None
2021-10-05 16:39:38 +00:00
2022-01-16 22:31:42 +00:00
override suspend fun getMainPage(): HomePageResponse {
val html = app.get("$mainUrl/home").text
2021-10-05 16:39:38 +00:00
val document = Jsoup.parse(html)
val all = ArrayList<HomePageList>()
val map = mapOf(
"Trending Movies" to "div#trending-movies",
"Trending TV Shows" to "div#trending-tv",
)
map.forEach {
all.add(HomePageList(
it.key,
2021-10-19 20:17:06 +00:00
document.select(it.value).select("div.film-poster").map { element ->
element.toSearchResult()
2021-10-05 16:39:38 +00:00
}
))
}
document.select("section.block_area.block_area_home.section-id-02").forEach {
val title = it.select("h2.cat-heading").text().trim()
2021-10-19 20:17:06 +00:00
val elements = it.select("div.film-poster").map { element ->
element.toSearchResult()
2021-10-05 16:39:38 +00:00
}
all.add(HomePageList(title, elements))
}
return HomePageResponse(all)
}
2022-01-16 22:31:42 +00:00
override suspend fun search(query: String): List<SearchResponse> {
2021-10-05 17:06:11 +00:00
val url = "$mainUrl/search/${query.replace(" ", "-")}"
val html = app.get(url).text
2021-10-05 16:39:38 +00:00
val document = Jsoup.parse(html)
return document.select("div.flw-item").map {
val title = it.select("h2.film-name").text()
val href = fixUrl(it.select("a").attr("href"))
val year = it.select("span.fdi-item").text().toIntOrNull()
val image = it.select("img").attr("data-src")
val isMovie = href.contains("/movie/")
if (isMovie) {
MovieSearchResponse(
title,
href,
this.name,
TvType.Movie,
image,
year
)
} else {
TvSeriesSearchResponse(
title,
href,
this.name,
TvType.TvSeries,
image,
year,
null
)
}
}
}
2022-01-16 22:31:42 +00:00
override suspend fun load(url: String): LoadResponse {
2022-01-09 00:23:35 +00:00
val document = app.get(url).document
2021-10-05 16:39:38 +00:00
val details = document.select("div.detail_page-watch")
val img = details?.select("img.film-poster-img")
val posterUrl = img?.attr("src")
val title = img?.attr("title") ?: throw ErrorLoadingException("No Title")
/*
2021-10-05 16:39:38 +00:00
val year = Regex("""[Rr]eleased:\s*(\d{4})""").find(
document.select("div.elements").text()
)?.groupValues?.get(1)?.toIntOrNull()
val duration = Regex("""[Dd]uration:\s*(\d*)""").find(
document.select("div.elements").text()
)?.groupValues?.get(1)?.trim()?.plus(" min")*/
var duration = document.selectFirst(".fs-item > .duration")?.text()?.trim()
var year: Int? = null
var tags: List<String>? = null
var cast: List<String>? = null
document.select("div.elements > .row > div > .row-line")?.forEach { element ->
val type = element?.select(".type")?.text() ?: return@forEach
when {
type.contains("Released") -> {
year = Regex("\\d+").find(
element.ownText() ?: return@forEach
)?.groupValues?.firstOrNull()?.toIntOrNull()
}
type.contains("Genre") -> {
tags = element.select("a")?.mapNotNull { it.text() }
}
type.contains("Cast") -> {
cast = element.select("a")?.mapNotNull { it.text() }
}
type.contains("Duration") -> {
duration = duration ?: element.ownText()?.trim()
}
}
}
val plot = details.select("div.description")?.text()?.replace("Overview:", "")?.trim()
2021-10-05 16:39:38 +00:00
val isMovie = url.contains("/movie/")
// https://sflix.to/movie/free-never-say-never-again-hd-18317 -> 18317
val idRegex = Regex(""".*-(\d+)""")
val dataId = details.attr("data-id")
val id = if (dataId.isNullOrEmpty())
2022-01-09 00:23:35 +00:00
idRegex.find(url)?.groupValues?.get(1)
?: throw RuntimeException("Unable to get id from '$url'")
2021-10-05 16:39:38 +00:00
else dataId
2022-02-04 20:49:35 +00:00
val recommendations =
document.select("div.film_list-wrap > div.flw-item")?.mapNotNull { element ->
val titleHeader =
element.select("div.film-detail > .film-name > a") ?: return@mapNotNull null
val recUrl = fixUrlNull(titleHeader.attr("href")) ?: return@mapNotNull null
val recTitle = titleHeader.text() ?: return@mapNotNull null
val poster = element.select("div.film-poster > img")?.attr("data-src")
MovieSearchResponse(
recTitle,
recUrl,
this.name,
if (recUrl.contains("/movie/")) TvType.Movie else TvType.TvSeries,
poster,
year = null
)
}
2021-10-05 16:39:38 +00:00
if (isMovie) {
// Movies
val episodesUrl = "$mainUrl/ajax/movie/episodes/$id"
val episodes = app.get(episodesUrl).text
2021-10-05 16:39:38 +00:00
// Supported streams, they're identical
2022-01-16 22:31:42 +00:00
val sourceIds = Jsoup.parse(episodes).select("a").mapNotNull { element ->
var sourceId = element.attr("data-id")
if (sourceId.isNullOrEmpty())
sourceId = element.attr("data-linkid")
2022-01-17 16:17:19 +00:00
if (element.select("span")?.text()?.trim()?.isValidServer() == true) {
if (sourceId.isNullOrEmpty()) {
fixUrlNull(element.attr("href"))
} else {
"$url.$sourceId".replace("/movie/", "/watch-movie/")
}
2022-01-16 22:31:42 +00:00
} else {
null
}
}
2021-10-05 16:39:38 +00:00
2022-01-16 22:31:42 +00:00
return newMovieLoadResponse(title, url, TvType.Movie, sourceIds.toJson()) {
2021-12-13 18:41:33 +00:00
this.year = year
this.posterUrl = posterUrl
this.plot = plot
setDuration(duration)
2022-02-05 23:58:56 +00:00
addActors(cast)
this.tags = tags
2022-02-04 20:49:35 +00:00
this.recommendations = recommendations
2021-12-13 18:41:33 +00:00
}
2021-10-05 16:39:38 +00:00
} else {
2022-01-09 00:23:35 +00:00
val seasonsDocument = app.get("$mainUrl/ajax/v2/tv/seasons/$id").document
2021-10-05 16:39:38 +00:00
val episodes = arrayListOf<TvSeriesEpisode>()
var seasonItems = seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a")
if (seasonItems.isNullOrEmpty())
seasonItems = seasonsDocument.select("div.dropdown-menu > a.dropdown-item")
seasonItems?.forEachIndexed { season, element ->
val seasonId = element.attr("data-id")
if (seasonId.isNullOrBlank()) return@forEachIndexed
var episode = 0
val seasonEpisodes = app.get("$mainUrl/ajax/v2/season/episodes/$seasonId").document
var seasonEpisodesItems =
seasonEpisodes.select("div.flw-item.film_single-item.episode-item.eps-item")
if (seasonEpisodesItems.isNullOrEmpty()) {
seasonEpisodesItems =
seasonEpisodes.select("ul > li > a")
2022-01-09 00:23:35 +00:00
}
seasonEpisodesItems.forEach {
val episodeImg = it?.select("img")
val episodeTitle = episodeImg?.attr("title") ?: it.ownText()
val episodePosterUrl = episodeImg?.attr("src")
val episodeData = it.attr("data-id") ?: return@forEach
episode++
val episodeNum =
(it.select("div.episode-number")?.text()
?: episodeTitle).let { str ->
Regex("""\d+""").find(str)?.groupValues?.firstOrNull()
?.toIntOrNull()
} ?: episode
episodes.add(
TvSeriesEpisode(
episodeTitle?.removePrefix("Episode $episodeNum: "),
season + 1,
episodeNum,
Pair(url, episodeData).toJson(),
fixUrlNull(episodePosterUrl)
)
)
}
}
2022-01-09 00:23:35 +00:00
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) {
2021-12-13 18:41:33 +00:00
this.posterUrl = posterUrl
this.year = year
this.plot = plot
setDuration(duration)
2022-02-05 23:58:56 +00:00
addActors(cast)
this.tags = tags
2022-02-04 20:49:35 +00:00
this.recommendations = recommendations
2021-12-13 18:41:33 +00:00
}
2021-10-05 16:39:38 +00:00
}
}
data class Tracks(
@JsonProperty("file") val file: String?,
@JsonProperty("label") val label: String?,
@JsonProperty("kind") val kind: String?
)
data class Sources(
2021-10-05 16:39:38 +00:00
@JsonProperty("file") val file: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String?
)
data class SourceObject(
@JsonProperty("sources") val sources: List<Sources?>?,
@JsonProperty("sources_1") val sources1: List<Sources?>?,
@JsonProperty("sources_2") val sources2: List<Sources?>?,
@JsonProperty("sourcesBackup") val sourcesBackup: List<Sources?>?,
2021-10-05 16:39:38 +00:00
@JsonProperty("tracks") val tracks: List<Tracks?>?
)
2022-01-16 22:31:42 +00:00
override suspend fun loadLinks(
2021-10-05 16:39:38 +00:00
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
2022-01-16 22:31:42 +00:00
val urls = (tryParseJson<Pair<String, String>>(data)?.let { (prefix, server) ->
val episodesUrl = "$mainUrl/ajax/v2/episode/servers/$server"
2021-10-05 16:39:38 +00:00
// Supported streams, they're identical
2022-01-30 01:50:49 +00:00
app.get(episodesUrl).document.select("a").mapNotNull { element ->
2022-01-16 22:31:42 +00:00
val id = element?.attr("data-id") ?: return@mapNotNull null
2022-01-17 16:17:19 +00:00
if (element.select("span")?.text()?.trim()?.isValidServer() == true) {
2022-01-16 22:31:42 +00:00
"$prefix.$id".replace("/tv/", "/watch-tv/")
} else {
null
}
}
} ?: tryParseJson<List<String>>(data))?.distinct()
urls?.apmap { url ->
suspendSafeApiCall {
2022-02-11 09:17:04 +00:00
val resolved = WebViewResolver(
Regex("""/getSources"""),
// This is unreliable, generating my own link instead
// additionalUrls = listOf(Regex("""^.*transport=polling(?!.*sid=).*$"""))
).resolveUsingWebView(getRequestCreator(url))
// val extractorData = resolved.second.getOrNull(0)?.url?.toString()
// Some smarter ws11 or w10 selection might be required in the future.
val extractorData =
2022-02-14 13:37:17 +00:00
"https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling"
2022-02-11 09:17:04 +00:00
val sources = resolved.first?.let { app.baseClient.newCall(it).execute().text }
?: return@suspendSafeApiCall
val mapped = parseJson<SourceObject>(sources)
mapped.tracks?.forEach {
it?.toSubtitleFile()?.let { subtitleFile ->
subtitleCallback.invoke(subtitleFile)
}
2022-01-16 22:31:42 +00:00
}
2022-01-09 00:23:35 +00:00
listOf(
mapped.sources to "",
mapped.sources1 to "source 2",
mapped.sources2 to "source 3",
mapped.sourcesBackup to "source backup"
).forEach { (sources, sourceName) ->
sources?.forEach {
2022-02-11 09:17:04 +00:00
it?.toExtractorLink(this, sourceName, extractorData)?.forEach(callback)
}
2022-01-16 22:31:42 +00:00
}
2021-10-28 11:28:19 +00:00
}
}
2022-01-09 00:23:35 +00:00
2022-01-16 22:31:42 +00:00
return !urls.isNullOrEmpty()
}
2022-02-11 09:17:04 +00:00
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()
}
2022-02-14 12:50:05 +00:00
/**
* 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
2022-02-14 13:37:17 +00:00
* @return the data and if it is new.
2022-02-14 12:50:05 +00:00
* */
2022-02-14 13:37:17 +00:00
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
2022-02-14 12:50:05 +00:00
}
2022-02-14 13:37:17 +00:00
return data to false
2022-02-14 12:50:05 +00:00
}
2022-02-11 09:17:04 +00:00
override suspend fun extractorVerifierJob(extractorData: String?) {
if (extractorData == null) return
val headers = mapOf(
"Referer" to "https://rabbitstream.net/"
)
2022-02-14 12:50:05 +00:00
var data = negotiateNewSid(extractorData) ?: return
2022-02-11 09:17:04 +00:00
// 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
)
2022-02-14 12:50:05 +00:00
// .also { println("First get ${it.text}") }
2022-02-11 09:17:04 +00:00
.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
2022-02-14 13:37:17 +00:00
var newAuth = false
2022-02-11 09:17:04 +00:00
while (true) {
2022-02-14 13:37:17 +00:00
val authData =
when {
newAuth -> "40"
reconnect -> """42["_reconnect", "$reconnectSid"]"""
else -> authInt
}
2022-02-11 09:17:04 +00:00
val url = "${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}"
2022-02-14 13:37:17 +00:00
getUpdatedData(
app.post(url, data = authData, headers = headers),
data,
extractorData
).also {
newAuth = it.second
data = it.first
}
2022-02-11 09:17:04 +00:00
//.also { println("Sflix post job ${it.text}") }
Log.d(this.name, "Running Sflix job $url")
val time = measureTimeMillis {
// This acts as a timeout
val getResponse = app.get(
"${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}",
timeout = 60,
headers = headers
2022-02-14 12:50:05 +00:00
)
// .also { println("Sflix get job ${it.text}") }
if (getResponse.text.contains("sid")) {
2022-02-11 09:17:04 +00:00
reconnect = true
2022-02-14 12:50:05 +00:00
// println("Reconnecting")
2022-02-11 09:17:04 +00:00
}
}
// Always waits even if the get response is instant, to prevent a while true loop.
if (time < interval - 4000)
2022-02-14 12:50:05 +00:00
delay(4000)
2022-02-11 09:17:04 +00:00
}
}
2022-01-16 22:31:42 +00:00
private fun Element.toSearchResult(): SearchResponse {
val img = this.select("img")
val title = img.attr("title")
val posterUrl = img.attr("data-src")
val href = fixUrl(this.select("a").attr("href"))
val isMovie = href.contains("/movie/")
return if (isMovie) {
MovieSearchResponse(
title,
href,
this@SflixProvider.name,
TvType.Movie,
posterUrl,
null
)
} else {
TvSeriesSearchResponse(
title,
href,
this@SflixProvider.name,
TvType.Movie,
posterUrl,
null,
null
)
2021-10-05 16:39:38 +00:00
}
}
companion object {
2022-01-16 22:31:42 +00:00
fun String?.isValidServer(): Boolean {
if (this.isNullOrEmpty()) return false
if (this.equals("UpCloud", ignoreCase = true) || this.equals(
"Vidcloud",
ignoreCase = true
) || this.equals("RapidStream", ignoreCase = true)
) return true
return false
2022-01-16 22:31:42 +00:00
}
2022-01-16 22:31:42 +00:00
// For re-use in Zoro
2022-02-11 09:17:04 +00:00
fun Sources.toExtractorLink(
caller: MainAPI,
name: String,
extractorData: String? = null
): List<ExtractorLink>? {
2021-10-19 20:17:06 +00:00
return this.file?.let { file ->
2022-01-16 22:31:42 +00:00
//println("FILE::: $file")
2022-01-09 00:23:35 +00:00
val isM3u8 = URI(this.file).path.endsWith(".m3u8") || this.type.equals(
"hls",
ignoreCase = true
)
if (isM3u8) {
2022-01-09 00:23:35 +00:00
M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true)
.map { stream ->
2022-01-16 22:31:42 +00:00
//println("stream: ${stream.quality} at ${stream.streamUrl}")
2022-01-09 00:23:35 +00:00
val qualityString = if ((stream.quality ?: 0) == 0) label
?: "" else "${stream.quality}p"
ExtractorLink(
caller.name,
"${caller.name} $qualityString $name",
stream.streamUrl,
caller.mainUrl,
getQualityFromName(stream.quality.toString()),
2022-02-11 09:17:04 +00:00
true,
extractorData = extractorData
2022-01-09 00:23:35 +00:00
)
}
} else {
listOf(ExtractorLink(
caller.name,
this.label?.let { "${caller.name} - $it" } ?: caller.name,
2021-10-19 20:17:06 +00:00
file,
caller.mainUrl,
getQualityFromName(this.type ?: ""),
false,
2022-02-11 09:17:04 +00:00
extractorData = extractorData
))
}
}
}
fun Tracks.toSubtitleFile(): SubtitleFile? {
return this.file?.let {
SubtitleFile(
this.label ?: "Unknown",
it
)
}
}
}
2021-10-05 17:06:11 +00:00
}