diff --git a/README.md b/README.md index f9e714e5..b887afd2 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ It merely scrapes 3rd-party websites that are publicly accessable via any regula - [vidembed.cc](https://vidembed.cc) - [vf-film.org](https://vf-film.org) - [asianload.cc](https://asianload.cc) +- [sflix.to](https://sflix.to) - [trailers.to](https://trailers.to) - [thenos.org](https://www.thenos.org) - [asiaflix.app](https://asiaflix.app) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 29b5924f..64a63cee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -45,6 +45,8 @@ object APIHolder { VidEmbedProvider(), VfFilmProvider(), AsianLoadProvider(), + + SflixProvider(), ) val restrictedApis = arrayListOf( diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt new file mode 100644 index 00000000..74dc91b4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SflixProvider.kt @@ -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 + 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() + + 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 { + 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() + + 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?, + @JsonProperty("sources_2") val sources2: List?, + @JsonProperty("tracks") val tracks: List? + ) + + 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(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 + } + +}