forked from recloudstream/cloudstream
Merge pull request #123 from Blatzar/master
This commit is contained in:
commit
96b5c9658d
3 changed files with 347 additions and 0 deletions
|
@ -58,6 +58,7 @@ It merely scrapes 3rd-party websites that are publicly accessable via any regula
|
||||||
- [vidembed.cc](https://vidembed.cc)
|
- [vidembed.cc](https://vidembed.cc)
|
||||||
- [vf-film.org](https://vf-film.org)
|
- [vf-film.org](https://vf-film.org)
|
||||||
- [asianload.cc](https://asianload.cc)
|
- [asianload.cc](https://asianload.cc)
|
||||||
|
- [sflix.to](https://sflix.to)
|
||||||
- [trailers.to](https://trailers.to)
|
- [trailers.to](https://trailers.to)
|
||||||
- [thenos.org](https://www.thenos.org)
|
- [thenos.org](https://www.thenos.org)
|
||||||
- [asiaflix.app](https://asiaflix.app)
|
- [asiaflix.app](https://asiaflix.app)
|
||||||
|
|
|
@ -45,6 +45,8 @@ object APIHolder {
|
||||||
VidEmbedProvider(),
|
VidEmbedProvider(),
|
||||||
VfFilmProvider(),
|
VfFilmProvider(),
|
||||||
AsianLoadProvider(),
|
AsianLoadProvider(),
|
||||||
|
|
||||||
|
SflixProvider(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val restrictedApis = arrayListOf(
|
val restrictedApis = arrayListOf(
|
||||||
|
|
|
@ -0,0 +1,344 @@
|
||||||
|
package com.lagradost.cloudstream3.movieproviders
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||||
|
import com.lagradost.cloudstream3.network.get
|
||||||
|
import com.lagradost.cloudstream3.network.text
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class SflixProvider : MainAPI() {
|
||||||
|
override val mainUrl: String
|
||||||
|
get() = "https://sflix.to"
|
||||||
|
override val name: String
|
||||||
|
get() = "Sflix"
|
||||||
|
|
||||||
|
override val hasQuickSearch: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val hasMainPage: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
override val hasChromecastSupport: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
override val hasDownloadSupport: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val supportedTypes: Set<TvType>
|
||||||
|
get() = setOf(
|
||||||
|
TvType.Movie,
|
||||||
|
TvType.TvSeries,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMainPage(): HomePageResponse? {
|
||||||
|
val html = get("$mainUrl/home").text
|
||||||
|
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,
|
||||||
|
document.select(it.value).select("div.film-poster").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
document.select("section.block_area.block_area_home.section-id-02").forEach {
|
||||||
|
val title = it.select("h2.cat-heading").text().trim()
|
||||||
|
val elements = it.select("div.film-poster").map {
|
||||||
|
it.toSearchResult()
|
||||||
|
}
|
||||||
|
all.add(HomePageList(title, elements))
|
||||||
|
}
|
||||||
|
|
||||||
|
return HomePageResponse(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val vpnStatus: VPNStatus
|
||||||
|
get() = VPNStatus.None
|
||||||
|
|
||||||
|
override fun search(query: String): List<SearchResponse> {
|
||||||
|
val url = "$mainUrl/search/${query.replace(" ", "-")}"
|
||||||
|
val html = get(url).text
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun load(url: String): LoadResponse? {
|
||||||
|
val html = get(url).text
|
||||||
|
val document = Jsoup.parse(html)
|
||||||
|
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
|
||||||
|
val plot = details.select("div.description").text().replace("Overview:", "").trim()
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
idRegex.find(url)?.groupValues?.get(1) ?: throw RuntimeException("Unable to get id from '$url'")
|
||||||
|
else dataId
|
||||||
|
|
||||||
|
if (isMovie) {
|
||||||
|
// Movies
|
||||||
|
val episodesUrl = "$mainUrl/ajax/movie/episodes/$id"
|
||||||
|
val episodes = get(episodesUrl).text
|
||||||
|
|
||||||
|
// Supported streams, they're identical
|
||||||
|
val sourceId = Jsoup.parse(episodes).select("a").firstOrNull {
|
||||||
|
it.select("span").text().trim().equals("RapidStream", ignoreCase = true)
|
||||||
|
|| it.select("span").text().trim().equals("Vidcloud", ignoreCase = true)
|
||||||
|
}?.attr("data-id")
|
||||||
|
|
||||||
|
val webViewUrl = "$url${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/")
|
||||||
|
|
||||||
|
return MovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
this.name,
|
||||||
|
TvType.Movie,
|
||||||
|
webViewUrl,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
plot,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
duration,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val seasonsHtml = get("$mainUrl/ajax/v2/tv/seasons/$id").text
|
||||||
|
val seasonsDocument = Jsoup.parse(seasonsHtml)
|
||||||
|
val episodes = arrayListOf<TvSeriesEpisode>()
|
||||||
|
|
||||||
|
seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a").forEachIndexed { season, it ->
|
||||||
|
val seasonId = it.attr("data-id")
|
||||||
|
if (seasonId.isNullOrBlank()) return@forEachIndexed
|
||||||
|
|
||||||
|
val seasonHtml = get("$mainUrl/ajax/v2/season/episodes/$seasonId").text
|
||||||
|
val seasonDocument = Jsoup.parse(seasonHtml)
|
||||||
|
seasonDocument.select("div.flw-item.film_single-item.episode-item.eps-item")
|
||||||
|
.forEachIndexed { i, it ->
|
||||||
|
val episodeImg = it.select("img")
|
||||||
|
val episodeTitle = episodeImg.attr("title")
|
||||||
|
val episodePosterUrl = episodeImg.attr("src")
|
||||||
|
val episodeData = it.attr("data-id")
|
||||||
|
|
||||||
|
// val episodeNum =
|
||||||
|
// Regex("""\d+""").find(it.select("div.episode-number").text())?.groupValues?.get(1)
|
||||||
|
// ?.toIntOrNull()
|
||||||
|
|
||||||
|
episodes.add(
|
||||||
|
TvSeriesEpisode(
|
||||||
|
episodeTitle,
|
||||||
|
season + 1,
|
||||||
|
null,
|
||||||
|
"$url:::$episodeData",
|
||||||
|
fixUrl(episodePosterUrl)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return TvSeriesLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
this.name,
|
||||||
|
TvType.TvSeries,
|
||||||
|
episodes,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
plot,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
duration,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("file") val file: String?,
|
||||||
|
@JsonProperty("label") val label: String?,
|
||||||
|
@JsonProperty("kind") val kind: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sources1(
|
||||||
|
@JsonProperty("file") val file: String?,
|
||||||
|
@JsonProperty("type") val type: String?,
|
||||||
|
@JsonProperty("label") val label: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourceObject(
|
||||||
|
@JsonProperty("sources_1") val sources1: List<Sources1?>?,
|
||||||
|
@JsonProperty("sources_2") val sources2: List<Sources1?>?,
|
||||||
|
@JsonProperty("tracks") val tracks: List<Tracks?>?
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Sources1.toExtractorLink(): ExtractorLink? {
|
||||||
|
return this.file?.let {
|
||||||
|
ExtractorLink(
|
||||||
|
this@SflixProvider.name,
|
||||||
|
this.label?.let { "${this@SflixProvider.name} - $it" } ?: this@SflixProvider.name,
|
||||||
|
it,
|
||||||
|
this@SflixProvider.mainUrl,
|
||||||
|
getQualityFromName(this.label ?: ""),
|
||||||
|
URI(this.file).path.endsWith(".m3u8") || this.label.equals("hls", ignoreCase = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Tracks.toSubtitleFile(): SubtitleFile? {
|
||||||
|
return this.file?.let {
|
||||||
|
SubtitleFile(
|
||||||
|
this.label ?: "Unknown",
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
// To transfer url:::id
|
||||||
|
val split = data.split(":::")
|
||||||
|
// Only used for tv series
|
||||||
|
val url = if (split.size == 2) {
|
||||||
|
val episodesUrl = "$mainUrl/ajax/v2/episode/servers/${split[1]}"
|
||||||
|
val episodes = get(episodesUrl).text
|
||||||
|
|
||||||
|
// Supported streams, they're identical
|
||||||
|
val sourceId = Jsoup.parse(episodes).select("a").firstOrNull {
|
||||||
|
it.select("span").text().trim().equals("RapidStream", ignoreCase = true)
|
||||||
|
|| it.select("span").text().trim().equals("Vidcloud", ignoreCase = true)
|
||||||
|
}?.attr("data-id")
|
||||||
|
|
||||||
|
"${split[0]}${sourceId?.let { ".$it" } ?: ""}".replace("/tv/", "/watch-tv/")
|
||||||
|
} else {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = get(
|
||||||
|
url,
|
||||||
|
interceptor = WebViewResolver(
|
||||||
|
Regex("""/getSources""")
|
||||||
|
)
|
||||||
|
).text
|
||||||
|
|
||||||
|
val mapped = mapper.readValue<SourceObject>(sources)
|
||||||
|
|
||||||
|
mapped.sources1?.forEach {
|
||||||
|
it?.toExtractorLink()?.let { extractorLink ->
|
||||||
|
callback.invoke(
|
||||||
|
extractorLink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapped.sources2?.forEach {
|
||||||
|
it?.toExtractorLink()?.let { extractorLink ->
|
||||||
|
callback.invoke(
|
||||||
|
extractorLink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapped.tracks?.forEach {
|
||||||
|
it?.toSubtitleFile()?.let { subtitleFile ->
|
||||||
|
subtitleCallback.invoke(subtitleFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue