package com.lagradost.cloudstream3.movieproviders import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.network.WebViewResolver 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.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.getQualityFromName import org.jsoup.Jsoup import org.jsoup.nodes.Element import java.net.URI class SflixProvider(providerUrl: String, providerName: String) : MainAPI() { override val mainUrl = providerUrl override val name = providerName override val hasQuickSearch = false override val hasMainPage = true override val hasChromecastSupport = true override val hasDownloadSupport = true override val usesWebView = true override val supportedTypes = setOf( TvType.Movie, TvType.TvSeries, ) override val vpnStatus = VPNStatus.None override suspend fun getMainPage(): HomePageResponse { val html = app.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 { element -> element.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 { element -> element.toSearchResult() } all.add(HomePageList(title, elements)) } return HomePageResponse(all) } override suspend fun search(query: String): List { val url = "$mainUrl/search/${query.replace(" ", "-")}" val html = app.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 suspend fun load(url: String): LoadResponse { val document = app.get(url).document 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 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 ) } if (isMovie) { // Movies val episodesUrl = "$mainUrl/ajax/movie/episodes/$id" val episodes = app.get(episodesUrl).text // Supported streams, they're identical val sourceIds = Jsoup.parse(episodes).select("a").mapNotNull { element -> val sourceId = element.attr("data-id") ?: return@mapNotNull null if (element.select("span")?.text()?.trim()?.isValidServer() == true) { "$url.$sourceId".replace("/movie/", "/watch-movie/") } else { null } } return newMovieLoadResponse(title, url, TvType.Movie, sourceIds.toJson()) { this.year = year this.posterUrl = posterUrl this.plot = plot setDuration(duration) this.recommendations = recommendations } } else { val seasonsDocument = app.get("$mainUrl/ajax/v2/tv/seasons/$id").document val episodes = arrayListOf() seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a") .forEachIndexed { season, element -> val seasonId = element.attr("data-id") if (seasonId.isNullOrBlank()) return@forEachIndexed var episode = 0 app.get("$mainUrl/ajax/v2/season/episodes/$seasonId").document .select("div.flw-item.film_single-item.episode-item.eps-item") .forEach { val episodeImg = it.select("img") ?: return@forEach val episodeTitle = episodeImg.attr("title") ?: return@forEach val episodePosterUrl = episodeImg.attr("src") ?: return@forEach 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(), fixUrl(episodePosterUrl) ) ) } } return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { this.posterUrl = posterUrl this.year = year this.plot = plot setDuration(duration) this.recommendations = recommendations } } } data class Tracks( @JsonProperty("file") val file: String?, @JsonProperty("label") val label: String?, @JsonProperty("kind") val kind: String? ) data class Sources( @JsonProperty("file") val file: String?, @JsonProperty("type") val type: String?, @JsonProperty("label") val label: String? ) data class SourceObject( @JsonProperty("sources") val sources: List?, @JsonProperty("sources_1") val sources1: List?, @JsonProperty("sources_2") val sources2: List?, @JsonProperty("sourcesBackup") val sourcesBackup: List?, @JsonProperty("tracks") val tracks: List? ) override suspend fun loadLinks( data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ): Boolean { val urls = (tryParseJson>(data)?.let { (prefix, server) -> val episodesUrl = "$mainUrl/ajax/v2/episode/servers/$server" // Supported streams, they're identical app.get(episodesUrl).document.select("a").mapNotNull { element -> val id = element?.attr("data-id") ?: return@mapNotNull null if (element.select("span")?.text()?.trim()?.isValidServer() == true) { "$prefix.$id".replace("/tv/", "/watch-tv/") } else { null } } } ?: tryParseJson>(data))?.distinct() urls?.apmap { url -> val sources = app.get( url, interceptor = WebViewResolver( Regex("""/getSources"""), ) ).text val mapped = parseJson(sources) mapped.tracks?.forEach { it?.toSubtitleFile()?.let { subtitleFile -> subtitleCallback.invoke(subtitleFile) } } listOf( mapped.sources to "", mapped.sources1 to "source 2", mapped.sources2 to "source 3", mapped.sourcesBackup to "source backup" ).forEach { (sources, sourceName) -> sources?.forEach { it?.toExtractorLink(this, sourceName)?.forEach(callback) } } } return !urls.isNullOrEmpty() } 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 ) } } companion object { 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 } // For re-use in Zoro fun Sources.toExtractorLink(caller: MainAPI, name: String): List? { return this.file?.let { file -> //println("FILE::: $file") val isM3u8 = URI(this.file).path.endsWith(".m3u8") || this.type.equals( "hls", ignoreCase = true ) if (isM3u8) { M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true) .map { stream -> //println("stream: ${stream.quality} at ${stream.streamUrl}") 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()), true ) } } else { listOf(ExtractorLink( caller.name, this.label?.let { "${caller.name} - $it" } ?: caller.name, file, caller.mainUrl, getQualityFromName(this.type ?: ""), false, )) } } } fun Tracks.toSubtitleFile(): SubtitleFile? { return this.file?.let { SubtitleFile( this.label ?: "Unknown", it ) } } } }