package com.streamflixreborn.streamflix.providers import com.tanasi.retrofit_jsoup.converter.JsoupConverterFactory import com.streamflixreborn.streamflix.adapters.AppAdapter import com.streamflixreborn.streamflix.extractors.Extractor import com.streamflixreborn.streamflix.models.Category import com.streamflixreborn.streamflix.models.Episode import com.streamflixreborn.streamflix.models.Genre import com.streamflixreborn.streamflix.models.Movie import com.streamflixreborn.streamflix.models.People import com.streamflixreborn.streamflix.models.Season import com.streamflixreborn.streamflix.models.TvShow import com.streamflixreborn.streamflix.models.Video import com.streamflixreborn.streamflix.utils.DnsResolver import java.util.concurrent.TimeUnit import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.OkHttpClient import org.jsoup.nodes.Document import retrofit2.HttpException import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import com.streamflixreborn.streamflix.utils.UserPreferences import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path object WiflixProvider : Provider, ProviderPortalUrl, ProviderConfigUrl { override val name = "Wiflix" override val defaultPortalUrl: String = "https://ww1.wiflix-adresses.fun/" override val portalUrl: String = defaultPortalUrl get() { val cachePortalURL = UserPreferences.getProviderCache(this, UserPreferences.PROVIDER_PORTAL_URL) return cachePortalURL.ifEmpty{ field } } override val defaultBaseUrl: String = "https://flemmix.team/" override val baseUrl: String = defaultBaseUrl get() { val cacheURL = UserPreferences.getProviderCache(this, UserPreferences.PROVIDER_URL) return cacheURL.ifEmpty{ field } } override val logo: String get() { var cacheLogo = UserPreferences.getProviderCache(this,UserPreferences.PROVIDER_LOGO) return cacheLogo.ifEmpty { "" } } override val language = "fr" override val changeUrlMutex = Mutex() private lateinit var service: Service private var serviceInitialized = false private val initializationMutex = Mutex() // Flag to track if more search results are available. Set to false when API returns fewer items than requested. // This prevents querying non-existent pages that could return random/incorrect results. private var hasMore = true override suspend fun getHome(): List { initializeService() val document = service.getHome() val categories = mutableListOf() categories.add( Category( name = "TOP Séries", list = document.select("div.block-main").getOrNull(0)?.select("div.mov")?.map { TvShow( id = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "", title = listOfNotNull( it.selectFirst("a.mov-t")?.text(), it.selectFirst("span.block-sai")?.text(), ).joinToString(" - "), poster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src }, ) } ?: emptyList(), ) ) categories.add( Category( name = "TOP Films", list = document.select("div.block-main").getOrNull(1)?.select("div.mov")?.map { Movie( id = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "", title = it.selectFirst("a.mov-t") ?.text() ?: "", poster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src }, ) } ?: emptyList(), ) ) categories.add( Category( name = "Films Anciens", list = document.select("div.block-main").getOrNull(2)?.select("div.mov")?.map { Movie( id = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "", title = it.selectFirst("a.mov-t") ?.text() ?: "", poster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src }, ) } ?: emptyList(), ) ) return categories } suspend fun ignoreSource(source: String): Boolean { if (arrayOf("netu", "vudeo").any { it.equals(source, true)}) return true return false } override suspend fun search(query: String, page: Int): List { initializeService() if (query.isEmpty()) { val document = service.getHome() val genres = document.select("div.side-b").getOrNull(1)?.select("ul li")?.map { Genre( id = it.selectFirst("a") ?.attr("href")?.substringBeforeLast("/")?.substringAfterLast("/") ?: "", name = it.selectFirst("a") ?.text() ?: "", ) } ?: emptyList() return genres } if (page > 1 && !hasMore) return emptyList() val document = service.search( story = query, searchStart = page, resultFrom = 1 + 20 * (page - 1) ) document.selectFirst("div.berrors")?.text()?.let { resultText -> val totalResults = resultText.substringAfter("trouvé ") .substringBefore(" réponses").toIntOrNull() ?: 0 val currentRange = resultText.substringAfter("Résultats de la requête ") .substringBefore(")").split(" - ") val receivedItems = currentRange.getOrNull(1)?.toIntOrNull() ?: 0 hasMore = receivedItems < totalResults } val results = document.select("div.mov").mapNotNull { val showId = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "" val showPoster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src } val href = it.selectFirst("a.mov-t") ?.attr("href") ?: "" if (href.contains("film-en-streaming/") || href.contains("film-ancien/")) { Movie( id = showId, title = it.selectFirst("a.mov-t") ?.text() ?: "", poster = showPoster, ) } else if (href.contains("serie-en-streaming/") || href.contains("vf/")) { TvShow( id = showId, title = listOfNotNull( it.selectFirst("a.mov-t")?.text(), it.selectFirst("span.block-sai")?.text(), ).joinToString(" - "), poster = showPoster, ) } else { null } } return results } override suspend fun getMovies(page: Int): List { initializeService() val document = service.getMovies(page) val movies = document.select("div.mov").map { Movie( id = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "", title = it.selectFirst("a.mov-t") ?.text() ?: "", poster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src }, ) } return movies } override suspend fun getTvShows(page: Int): List { initializeService() val document = service.getTvShows(page) val tvShows = document.select("div.mov").map { TvShow( id = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "", title = listOfNotNull( it.selectFirst("a.mov-t")?.text(), it.selectFirst("span.block-sai")?.text(), ).joinToString(" - "), poster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src }, ) } return tvShows } override suspend fun getMovie(id: String): Movie { initializeService() val document = service.getMovie(id) val movie = Movie( id = id, title = document.selectFirst("header.full-title h1") ?.text() ?: "", overview = document.selectFirst("div.screenshots-full") ?.ownText() ?.substringAfter("en Streaming Complet:") ?.trim(), released = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("Date de sortie") == true } ?.selectFirst("div.mov-desc") ?.text()?.trim(), runtime = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("Durée") == true } ?.selectFirst("div.mov-desc") ?.text()?.let { val hours = it.substringBefore("h").toIntOrNull() ?: 0 val minutes = it.substringBeforeLast("min").substringAfterLast("h").trim().toIntOrNull() ?: 0 hours * 60 + minutes }?.takeIf { it != 0 }, quality = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("Qualité") == true } ?.selectFirst("div.mov-desc") ?.text(), poster = document.selectFirst("img#posterimg") ?.attr("src")?.let { baseUrl + it }, genres = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("GENRE") == true } ?.select("div.mov-desc a")?.mapNotNull { if (it.text() == "Film") return@mapNotNull null Genre( id = it.attr("href").substringBeforeLast("/").substringAfterLast("/"), name = it.text(), ) } ?: emptyList(), directors = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("ALISATEUR") == true } ?.selectFirst("div.mov-desc span") ?.let { element -> element.text() .split(", ") .mapIndexed { index, name -> People( id = "director$index", name = name, ) } } ?: emptyList(), cast = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("ACTEURS") == true } ?.select("div.mov-desc a")?.map { People( id = it.attr("href").substringBeforeLast("/").substringAfterLast("/"), name = it.text(), ) } ?: emptyList(), recommendations = document.select("div.related div.item").mapNotNull { if (it.hasClass("cloned")) return@mapNotNull null val showId = it.selectFirst("a") ?.attr("href")?.substringAfterLast("/") ?: "" val showTitle = it.selectFirst("span.title1") ?.text() ?: "" val showPoster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src } val href = it.selectFirst("a") ?.attr("href") ?: "" if (href.contains("film-en-streaming/") || href.contains("film-ancien/")) { Movie( id = showId, title = showTitle, poster = showPoster, ) } else if (href.contains("serie-en-streaming/") || href.contains("vf/")) { TvShow( id = showId, title = showTitle, poster = showPoster, ) } else { null } } ) return movie } override suspend fun getTvShow(id: String): TvShow { initializeService() val document = service.getTvShow(id) val title = document.selectFirst("header.full-title h1") ?.text() ?: "" val seasonNumber = title.substringAfter("Saison ").trim().toIntOrNull() ?: 0 val tvShow = TvShow( id = id, title = title, overview = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("Synopsis") == true } ?.selectFirst("div.mov-desc") ?.text() ?.substringAfter("en Streaming Complet:") ?.trim(), released = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("Date de sortie") == true } ?.selectFirst("div.mov-desc") ?.text()?.trim(), runtime = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("Durée") == true } ?.selectFirst("div.mov-desc") ?.text()?.let { val hours = it.substringBefore("h").toIntOrNull() ?: 0 val minutes = it.substringBeforeLast(" mn").substringAfterLast(" ").toIntOrNull() ?: 0 hours * 60 + minutes }?.takeIf { it != 0 }, poster = document.selectFirst("img#posterimg") ?.attr("src")?.let { baseUrl + it }, seasons = listOfNotNull( Season( id = "$id/blocvostfr", title = "Épisodes - VOSTFR", number = seasonNumber ).takeIf { document.select("div.blocvostfr ul.eplist li").size > 0 }, Season( id = "$id/blocfr", title = "Épisodes - VF", number = seasonNumber ).takeIf { document.select("div.blocfr ul.eplist li").size > 0 }, ), directors = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("ALISATEUR") == true } ?.selectFirst("div.mov-desc span") ?.let { element -> element.text() .split(", ") .mapIndexed { index, name -> People( id = "director$index", name = name, ) } } ?: emptyList(), cast = document.select("ul.mov-list li") .find { it.selectFirst("div.mov-label")?.text()?.contains("ACTEURS") == true } ?.select("div.mov-desc a")?.map { People( id = it.attr("href").substringBeforeLast("/").substringAfterLast("/"), name = it.text(), ) } ?: emptyList(), recommendations = document.select("div.related div.item").mapNotNull { if (it.hasClass("cloned")) return@mapNotNull null val showId = it.selectFirst("a") ?.attr("href")?.substringAfterLast("/") ?: "" val showTitle = it.selectFirst("span.title1") ?.text() ?: "" val showPoster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src } val href = it.selectFirst("a") ?.attr("href") ?: "" if (href.contains("film-en-streaming/") || href.contains("film-ancien/")) { Movie( id = showId, title = showTitle, poster = showPoster, ) } else if (href.contains("serie-en-streaming/") || href.contains("vf/")) { TvShow( id = showId, title = showTitle, poster = showPoster, ) } else { null } } ) return tvShow } override suspend fun getEpisodesBySeason(seasonId: String): List { initializeService() val (tvShowId, className) = seasonId.split("/") val document = service.getTvShow(tvShowId) val episodes = document.select("div.$className ul.eplist li").map { Episode( id = "$tvShowId/${it.attr("rel")}", number = it.text().substringAfter("Episode ").toIntOrNull() ?: 0, title = it.text(), ) } return episodes } override suspend fun getGenre(id: String, page: Int): Genre { initializeService() val document = service.getGenre(id, page) val genre = Genre( id = id, name = "", shows = document.select("div.mov").map { Movie( id = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "", title = it.selectFirst("a.mov-t") ?.text() ?: "", poster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src }, ) }, ) return genre } override suspend fun getPeople(id: String, page: Int): People { initializeService() val document = try { service.getPeople(id, page) } catch (e: HttpException) { when (e.code()) { 404 -> return People(id, "") else -> throw e } } val people = People( id = id, name = "", filmography = document.select("div.mov").mapNotNull { val showId = it.selectFirst("a.mov-t") ?.attr("href")?.substringAfterLast("/") ?: "" val showPoster = it.selectFirst("img") ?.attr("src")?.let { src -> baseUrl + src } val href = it.selectFirst("a.mov-t") ?.attr("href") ?: "" if (href.contains("film-en-streaming/") || href.contains("film-ancien/")) { Movie( id = showId, title = it.selectFirst("a.mov-t") ?.text() ?: "", poster = showPoster, ) } else if (href.contains("serie-en-streaming/") || href.contains("vf/")) { TvShow( id = showId, title = listOfNotNull( it.selectFirst("a.mov-t")?.text(), it.selectFirst("span.block-sai")?.text(), ).joinToString(" - "), poster = showPoster, ) } else { null } }, ) return people } override suspend fun getServers(id: String, videoType: Video.Type): List { initializeService() val servers = when (videoType) { is Video.Type.Episode -> { val (tvShowId, rel) = id.split("/") val document = service.getTvShow(tvShowId) document.select("div.$rel a"). filter { ignoreSource(it.text().trim() ) == false }. mapIndexed { index, it -> Video.Server( id = it.selectFirst("span") ?.text() ?: index.toString(), name = it.selectFirst("span") ?.text() ?: "", src = it.attr("onclick") .substringAfter("loadVideo('").substringBeforeLast("'"), ) } } is Video.Type.Movie -> { val document = service.getMovie(id) document.select("div.tabs-sel a"). filter { ignoreSource(it.text().trim() ) == false }. mapIndexed { index, it -> Video.Server( id = it.selectFirst("span") ?.text() ?: index.toString(), name = it.selectFirst("span") ?.text() ?: "", src = it.attr("onclick") .substringAfter("loadVideo('").substringBeforeLast("'"), ) } } } return servers } override suspend fun getVideo(server: Video.Server): Video { val video = Extractor.extract(server.src) return video } /** * Initializes the service with the current domain URL. * This function is necessary because the provider's domain frequently changes. * We fetch the latest URL from a dedicated website that tracks these changes. */ override suspend fun onChangeUrl(forceRefresh: Boolean): String { changeUrlMutex.withLock { if (forceRefresh || UserPreferences.getProviderCache(this,UserPreferences.PROVIDER_AUTOUPDATE) != "false") { val addressService = Service.buildAddressFetcher() try { val document = addressService.getHome() // Cerchiamo l'URL tra i vari elementi che solitamente contengono il link attivo val newUrl = document.select("div.card-featured-wrap a.card-featured, div.alert-success a, div.alert-info a, a.btn-success, div.entry-content a") .map { it.attr("href").trim() } .firstOrNull { link -> // Escludiamo i link interni, i social e il portale stesso link.startsWith("http") && !link.contains("wiflix-adresses") && !link.contains("facebook") && !link.contains("twitter") && !link.contains("t.me") && !link.contains("instagram") && !link.contains("pinterest") } ?.replace("http://", "https://") if (!newUrl.isNullOrEmpty()) { val formattedUrl = if (newUrl.endsWith("/")) newUrl else "$newUrl/" UserPreferences.setProviderCache(this, UserPreferences.PROVIDER_URL, formattedUrl) UserPreferences.setProviderCache( this, UserPreferences.PROVIDER_LOGO, formattedUrl + "templates/flemmixnew/images/favicon.png" ) } } catch (e: Exception) { // In case of failure, we'll use the default URL // No need to throw as we already have a fallback URL } } service = Service.build(baseUrl) serviceInitialized = true } return baseUrl } private suspend fun initializeService() { initializationMutex.withLock { if (serviceInitialized) return onChangeUrl() } } private interface Service { companion object { private val client = OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS) .dns(DnsResolver.doh) .build() fun buildAddressFetcher(): Service { val addressRetrofit = Retrofit.Builder() .baseUrl(portalUrl) .addConverterFactory(JsoupConverterFactory.create()) .client(client) .build() return addressRetrofit.create(Service::class.java) } fun build(baseUrl: String): Service { val retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(JsoupConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .client(client) .build() return retrofit.create(Service::class.java) } } @GET(".") suspend fun getHome(): Document @POST("index.php?do=search") @FormUrlEncoded suspend fun search( @Field("story") story: String, @Field("do") doo: String = "search", @Field("subaction") subaction: String = "search", @Field("search_start") searchStart: Int = 0, @Field("full_search") fullSearch: Int = 0, @Field("result_from") resultFrom: Int = 1, ): Document @GET("film-en-streaming/page/{page}") suspend fun getMovies(@Path("page") page: Int): Document @GET("serie-en-streaming/page/{page}") suspend fun getTvShows(@Path("page") page: Int): Document @GET("film-en-streaming/{id}") suspend fun getMovie(@Path("id") id: String): Document @GET("serie-en-streaming/{id}") suspend fun getTvShow(@Path("id") id: String): Document @GET("film-en-streaming/{genre}/page/{page}") suspend fun getGenre( @Path("genre") genre: String, @Path("page") page: Int, ): Document @GET("xfsearch/acteurs/{id}/page/{page}") suspend fun getPeople( @Path("id") id: String, @Path("page") page: Int, ): Document } }