package com.streamflixreborn.streamflix.providers import com.tanasi.retrofit_jsoup.converter.JsoupConverterFactory import com.streamflixreborn.streamflix.adapters.AppAdapter 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.Show import com.streamflixreborn.streamflix.models.TvShow import com.streamflixreborn.streamflix.models.Video import com.streamflixreborn.streamflix.extractors.Extractor import com.streamflixreborn.streamflix.utils.DnsResolver import com.streamflixreborn.streamflix.utils.TmdbUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.ResponseBody import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.dnsoverhttps.DnsOverHttps import retrofit2.Response import org.jsoup.nodes.Document import org.jsoup.Jsoup import retrofit2.Retrofit import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Url import retrofit2.http.Header import retrofit2.http.Body import java.net.HttpURLConnection import java.net.URLDecoder import java.net.URLEncoder import java.util.concurrent.TimeUnit object StreamingItaProvider : Provider { override val name = "StreamingIta" override val baseUrl = "https://streamingita.homes" override val language = "it" override val logo: String get() = "$baseUrl/wp-content/uploads/2019/204/logos.png" private const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" private fun getOkHttpClient(): OkHttpClient { val clientBuilder = OkHttpClient.Builder() .readTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS) return clientBuilder.dns(DnsResolver.doh).build() } private val retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(JsoupConverterFactory.create()) .client(getOkHttpClient()) .build() private val service = retrofit.create(Service::class.java) private interface Service { @GET suspend fun getPage(@Url url: String): Document @POST("wp-admin/admin-ajax.php") suspend fun getPlayerAjax( @Header("Referer") referer: String, @Body body: FormBody ): Response } override suspend fun getHome(): List { return try { val document = service.getPage(baseUrl) // Top slider val sliderItems = coroutineScope { document.select("#slider-movies-tvshows article.item").map { el -> async { val href = el.selectFirst(".image a")?.attr("href").orEmpty() val img = el.selectFirst(".image img")?.attr("src") val title = el.selectFirst(".data h3.title, .data h3 a, h3.title")?.text() ?: el.selectFirst("img")?.attr("alt") ?: "" val tmdbRating = when { href.contains("/film/") -> TmdbUtils.getMovie(title, language = language)?.rating href.contains("/tv/") -> TmdbUtils.getTvShow(title, language = language)?.rating else -> null } when { href.contains("/film/") -> Movie(id = href, title = title, poster = img, banner = img, rating = tmdbRating) href.contains("/tv/") -> TvShow(id = href, title = title, poster = img, banner = img, rating = tmdbRating) else -> null } } }.awaitAll().filterNotNull() } val categories = mutableListOf() if (sliderItems.isNotEmpty()) categories.add(Category(name = Category.FEATURED, list = sliderItems)) val contentRoot = document.selectFirst("div.content.full_width_layout.normal") ?: document contentRoot.select("h2").forEach { headerEl -> val categoryName = headerEl.text().trim() val headerBlock = headerEl.parent() ?: headerEl var container = headerBlock.nextElementSibling() while (container != null && !(container.hasClass("items") || container.id() == "featured-titles" || container.id() == "dt-movies" || container.id() == "dt-tvshows")) { container = container.nextElementSibling() } if (container != null) { val items = container.select("article.item").mapNotNull { el -> val href = el.selectFirst("a")?.attr("href").orEmpty() if (href.isBlank()) return@mapNotNull null val img = el.selectFirst(".poster img, .image img, img")?.attr("src") val title = el.selectFirst(".data h3 a, .data h3, h3 a, h3.title")?.text() ?: el.selectFirst("img")?.attr("alt") ?: "" when { href.contains("/tv/") -> TvShow(id = href, title = title, poster = img) href.contains("/film/") -> Movie(id = href, title = title, poster = img) else -> null } } if (items.isNotEmpty()) { categories.add(Category(name = categoryName, list = items)) } } } categories } catch (e: Exception) { emptyList() } } override suspend fun search(query: String, page: Int): List { if (query.isBlank()) { if (page > 1) return emptyList() return try { val document = service.getPage(baseUrl) val filmMenu = document.selectFirst("ul#main_header > li a[href='/film/']")?.closest("li") val genreLinks = filmMenu?.select("ul.sub-menu li a[href]") ?: emptyList() genreLinks .mapNotNull { a -> val href = a.attr("href").trim() val text = a.text().trim() if (href.isBlank() || text.isBlank()) return@mapNotNull null if (!href.contains("/genere/")) return@mapNotNull null Genre( id = if (href.startsWith("http")) href else baseUrl + href.removePrefix("/"), name = text ) } .sortedBy { it.name } } catch (_: Exception) { emptyList() } } return try { val encoded = URLEncoder.encode(query, "UTF-8") val url = if (page > 1) "$baseUrl/page/$page/?s=$encoded" else "$baseUrl/?s=$encoded" val document = service.getPage(url) document.select(".search-page .result-item article").mapNotNull { el -> val titleAnchor = el.selectFirst(".details .title > a") ?: return@mapNotNull null val href = titleAnchor.attr("href").orEmpty() if (href.isBlank()) return@mapNotNull null val title = titleAnchor.text().trim() val img = el.selectFirst(".thumbnail img")?.attr("src")?.replace("-150x150", "") when { href.contains("/film/") -> Movie(id = href, title = title, poster = img) href.contains("/tv/") -> TvShow(id = href, title = title, poster = img) else -> null } } } catch (_: Exception) { emptyList() } } override suspend fun getMovies(page: Int): List { return try { val url = if (page > 1) "$baseUrl/film/page/$page/" else "$baseUrl/film/" val document = service.getPage(url) // For pagination (page > 1), only use items from "Aggiunto recentemente" section // For page 1, include both "In Sala" and "Aggiunto recentemente" val itemsSelector = if (page > 1) { "#archive-content article.item" } else { "article.item" } document.select(itemsSelector).mapNotNull { el -> val href = el.selectFirst("a")?.attr("href").orEmpty() if (href.isBlank() || !href.contains("/film/")) return@mapNotNull null val img = el.selectFirst(".poster img, .image img, img")?.attr("src") val title = el.selectFirst(".data h3 a, .data h3, h3 a, h3.title")?.text() ?: el.selectFirst("img")?.attr("alt") ?: "" Movie( id = href, title = title, poster = img ) } } catch (e: Exception) { emptyList() } } override suspend fun getTvShows(page: Int): List { return try { val url = if (page > 1) "$baseUrl/tv/page/$page/" else "$baseUrl/tv/" val document = service.getPage(url) // For pagination (page > 1), only use items from "Aggiunto recentemente" section // For page 1, include both "Hot" and "Aggiunto recentemente" val itemsSelector = if (page > 1) { "#archive-content article.item" } else { "article.item" } document.select(itemsSelector).mapNotNull { el -> val href = el.selectFirst("a")?.attr("href").orEmpty() if (href.isBlank() || !href.contains("/tv/")) return@mapNotNull null val img = el.selectFirst(".poster img, .image img, img")?.attr("src") val title = el.selectFirst(".data h3 a, .data h3, h3 a, h3.title")?.text() ?: el.selectFirst("img")?.attr("alt") ?: "" TvShow( id = href, title = title, poster = img ) } } catch (e: Exception) { emptyList() } } override suspend fun getMovie(id: String): Movie { val document = service.getPage(id) val title = document.selectFirst("div.data > h1")?.text() ?: "" val tmdbMovie = TmdbUtils.getMovie(title, language = language) val poster = tmdbMovie?.poster ?: document.selectFirst("div.poster img[itemprop=image]")?.attr("src") ?: document.selectFirst("meta[property=og:image]")?.attr("content") return Movie( id = id, title = title, poster = poster, overview = tmdbMovie?.overview ?: document.selectFirst("#info .wp-content p")?.text(), rating = tmdbMovie?.rating ?: document.selectFirst(".starstruck-rating .dt_rating_vgs")?.text()?.replace(',', '.')?.toDoubleOrNull(), trailer = tmdbMovie?.trailer ?: document.selectFirst("#trailer iframe, #trailer .embed iframe")?.attr("src")?.let { normalizeUrl(it) }?.let { mapTrailerToWatchUrl(it) }, genres = tmdbMovie?.genres ?: document.select("div.sgeneros a[rel=tag]").map { Genre(it.text(), it.text()) }, cast = document.select("#cast h2:matches(^Cast$) + .persons .person").map { el -> val anchor = el.selectFirst(".data .name a") val name = anchor?.text() ?: el.selectFirst("[itemprop=name]")?.attr("content") ?: "" val img = el.selectFirst(".img img")?.attr("src") val href = anchor?.attr("href") val tmdbPerson = tmdbMovie?.cast?.find { it.name.equals(name, ignoreCase = true) } val personId = href?.let { h -> img?.let { i -> "$h?poster=${URLEncoder.encode(i, "UTF-8")}" } ?: h } People( id = personId ?: name, name = name, image = tmdbPerson?.image ?: img ) }, released = tmdbMovie?.released?.let { "${it.get(java.util.Calendar.YEAR)}-${it.get(java.util.Calendar.MONTH) + 1}-${it.get(java.util.Calendar.DAY_OF_MONTH)}" }, runtime = tmdbMovie?.runtime, banner = tmdbMovie?.banner, imdbId = tmdbMovie?.imdbId ) } override suspend fun getTvShow(id: String): TvShow { val document = service.getPage(id) val title = document.selectFirst("div.data > h1")?.text() ?: "" val tmdbTvShow = TmdbUtils.getTvShow(title, language = language) val poster = tmdbTvShow?.poster ?: document.selectFirst("div.poster img[itemprop=image]")?.attr("src") val seasons = document.select("#serie_contenido #seasons .se-c").map { seasonEl -> val seasonNumber = seasonEl.selectFirst(".se-q .se-t")?.text()?.trim()?.toIntOrNull() ?: 1 Season( id = "$id?season=$seasonNumber", number = seasonNumber, title = "Stagione $seasonNumber", poster = tmdbTvShow?.seasons?.find { it.number == seasonNumber }?.poster ) }.sortedBy { it.number } return TvShow( id = id, title = title, poster = poster, overview = tmdbTvShow?.overview ?: document.selectFirst("#info .wp-content p")?.text(), rating = tmdbTvShow?.rating ?: document.selectFirst(".starstruck-rating .dt_rating_vgs")?.text()?.replace(',', '.')?.toDoubleOrNull(), trailer = tmdbTvShow?.trailer ?: document.selectFirst("#trailer iframe, #trailer .embed iframe")?.attr("src")?.let { normalizeUrl(it) }?.let { mapTrailerToWatchUrl(it) }, seasons = seasons, genres = tmdbTvShow?.genres ?: document.select("div.sgeneros a[rel=tag]").map { Genre(it.text(), it.text()) }, cast = tmdbTvShow?.cast ?: document.select("#cast h2:matches(^Cast$) + .persons .person").map { el -> val anchor = el.selectFirst(".data .name a") val name = anchor?.text() ?: el.selectFirst("[itemprop=name]")?.attr("content") ?: "" val img = el.selectFirst(".img img")?.attr("src") val href = anchor?.attr("href") People(id = href ?: name, name = name, image = img) }, released = tmdbTvShow?.released?.let { "${it.get(java.util.Calendar.YEAR)}-${it.get(java.util.Calendar.MONTH) + 1}-${it.get(java.util.Calendar.DAY_OF_MONTH)}" }, runtime = tmdbTvShow?.runtime, banner = tmdbTvShow?.banner, imdbId = tmdbTvShow?.imdbId ) } override suspend fun getEpisodesBySeason(seasonId: String): List { val seasonNumber = seasonId.substringAfter("?season=").toIntOrNull() ?: 1 val pageUrl = seasonId.substringBefore("?season=") val document = service.getPage(pageUrl) val tmdbTvShow = TmdbUtils.getTvShow(document.selectFirst("div.data > h1")?.text() ?: "", language = language) val tmdbEpisodes = if (tmdbTvShow != null) TmdbUtils.getEpisodesBySeason(tmdbTvShow.id, seasonNumber, language = language) else emptyList() return document.select("#serie_contenido #seasons .se-c .se-a ul.episodios > li").mapNotNull { epEl -> val numText = epEl.selectFirst(".numerando")?.text()?.trim() ?: "" val seasonFromNum = numText.substringBefore("-").trim().toIntOrNull() if (seasonFromNum != seasonNumber) return@mapNotNull null val epNumber = numText.substringAfter("-").trim().toIntOrNull() ?: 0 val tmdbEp = tmdbEpisodes.find { it.number == epNumber } val epLink = epEl.selectFirst(".episodiotitle a")?.attr("href").orEmpty() val epTitle = tmdbEp?.title ?: epEl.selectFirst(".episodiotitle a")?.text() Episode( id = epLink, number = epNumber, title = epTitle, poster = tmdbEp?.poster ?: epEl.selectFirst(".imagen img")?.attr("src"), overview = tmdbEp?.overview ) } } override suspend fun getGenre(id: String, page: Int): Genre { // id รจ l'URL della pagina del genere return try { val base = if (id.startsWith("http")) id.removeSuffix("/") else "$baseUrl/${id.removePrefix("/").removeSuffix("/")}" val url = if (page > 1) "$base/page/$page/" else "$base/" val document = service.getPage(url) val name = document.selectFirst("h1, .archive-title, .data h1")?.text()?.trim() ?: id.substringAfterLast('/').replace('-', ' ') val shows = document.select("article.item").mapNotNull { el -> val href = el.selectFirst("a")?.attr("href").orEmpty() if (href.isBlank()) return@mapNotNull null val img = el.selectFirst(".poster img, .image img, img")?.attr("src") val title = el.selectFirst(".data h3 a, .data h3, h3 a, h3.title")?.text() ?: el.selectFirst("img")?.attr("alt") ?: "" // Considera solo i film per i generi di FILM STREAMING if (href.contains("/film/")) { Movie(id = href, title = title, poster = img) } else null } Genre( id = id, name = name, shows = shows ) } catch (e: Exception) { Genre(id = id, name = "", shows = emptyList()) } } override suspend fun getPeople(id: String, page: Int): People { return try { if (page > 1) { return People( id = id, name = id.substringAfterLast('/').substringBefore('?').replace('-', ' '), image = null, filmography = emptyList() ) } val posterFromId = id.substringAfter("poster=", "").substringBefore("&").let { raw -> if (raw.isBlank()) null else URLDecoder.decode(raw, "UTF-8") } val personUrl = id.substringBefore("?") val document = service.getPage(personUrl) val name = document.selectFirst(".data h1")?.text() ?: document.selectFirst("h1")?.text() ?: "" val poster = posterFromId ?: document.selectFirst(".poster img")?.attr("src") val filmography = parseMixedItems(document) People( id = id, name = name, image = poster, filmography = filmography ) } catch (e: Exception) { throw e } } private fun parseMixedItems(document: Document): List { return document.select("article.item").mapNotNull { el -> val href = el.selectFirst("a")?.attr("href").orEmpty() if (href.isBlank()) return@mapNotNull null val img = el.selectFirst(".poster img, .image img, img")?.attr("src") val title = el.selectFirst(".data h3 a, .data h3, h3 a, h3.title")?.text() ?: el.selectFirst("img")?.attr("alt") ?: "" when { href.contains("/tv/") -> TvShow(id = href, title = title, poster = img) href.contains("/film/") -> Movie(id = href, title = title, poster = img) else -> null } } } override suspend fun getServers(id: String, videoType: Video.Type): List { return try { val document = service.getPage(id) val postId = document.select("[data-post]").firstOrNull()?.attr("data-post") ?: document.select("#report-video").attr("data-post").ifBlank { null } ?: throw Exception("Post id not found") val contentType = document.select("[data-type]").firstOrNull()?.attr("data-type") ?: document.select("#report-video").attr("data-type").ifBlank { "movie" } ?: "movie" // First: Server1 val embedUrl1 = requestEmbedUrl(postId, nume = "1", type = contentType) val finalUrl1 = followRedirect(embedUrl1) val host1 = runCatching { finalUrl1.toHttpUrlOrNull()?.host } .getOrNull()?.replaceFirst("www.", "")?.substringBefore('.') ?.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } ?: "Server" val resultServers = mutableListOf( Video.Server( id = finalUrl1, name = host1, src = finalUrl1 ) ) // Then: Server2 (mirrors), if present val availableServers = document.select("[data-nume]").mapNotNull { it.attr("data-nume").toIntOrNull() }.toSet() val hasServer2 = availableServers.contains(2) if (hasServer2) { val serversFromMirrors = runCatching { val embedUrl2 = requestEmbedUrl(postId, nume = "2", type = contentType) val mirrorsPageUrl = followRedirect(embedUrl2) val mirrorsHtml = httpGet(mirrorsPageUrl, referer = id) val mirrorsDoc = Jsoup.parse(mirrorsHtml, mirrorsPageUrl) mirrorsDoc.select("ul._player-mirrors li[data-link]") .filterNot { li -> li.hasClass("fullhd") || li.text().contains("4K", true) } .mapNotNull { li -> val dataLink = li.attr("data-link").trim() if (dataLink.isBlank()) return@mapNotNull null val normalized = when { dataLink.startsWith("//") -> "https:$dataLink" dataLink.startsWith("http") -> dataLink else -> "https://$dataLink" } val nameText = li.ownText().ifBlank { li.text() }.trim() val name = nameText.ifBlank { normalized.toHttpUrlOrNull()?.host?.substringBefore('.') ?.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase() else c.toString() } ?: "Server" } Video.Server( id = normalized, name = name, src = normalized ) } // deduplicate by src, prefer the entry whose name contains "hd" .groupBy { it.src } .map { (_, list) -> list.maxByOrNull { server -> val n = server.name.lowercase() if ("hd" in n) 2 else 1 }!! } }.getOrDefault(emptyList()) if (serversFromMirrors.isNotEmpty()) resultServers.addAll(serversFromMirrors) } resultServers } catch (e: Exception) { emptyList() } } override suspend fun getVideo(server: Video.Server): Video { return Extractor.extract(server.id, server) } private suspend fun requestEmbedUrl(postId: String, nume: String, type: String): String { val formBody = FormBody.Builder() .add("action", "doo_player_ajax") .add("post", postId) .add("nume", nume) .add("type", type) .build() val response = service.getPlayerAjax(baseUrl, formBody) val body = response.body()?.string() ?: throw Exception("Empty response") val embed = org.json.JSONObject(body).optString("embed_url") if (embed.isNullOrBlank()) throw Exception("embed_url not found") return embed.replace("\\/", "/") } private suspend fun httpGet(url: String, referer: String? = null): String = withContext(Dispatchers.IO) { val client = OkHttpClient.Builder().followRedirects(true).followSslRedirects(true).build() val reqBuilder = Request.Builder().url(url).header("User-Agent", DEFAULT_USER_AGENT) referer?.let { reqBuilder.header("Referer", it) } client.newCall(reqBuilder.build()).execute().use { resp -> resp.body?.string() ?: "" } } private suspend fun followRedirect(url: String): String = withContext(Dispatchers.IO) { val client = OkHttpClient.Builder().followRedirects(true).followSslRedirects(true).build() val request = Request.Builder() .url(url) .header("Referer", baseUrl) .header("User-Agent", DEFAULT_USER_AGENT) .build() client.newCall(request).execute().use { response -> response.request.url.toString() } } private fun normalizeUrl(raw: String): String { val src = raw.trim() return when { src.startsWith("//") -> "https:$src" src.startsWith("http") -> src else -> "https://$src" } } private fun mapTrailerToWatchUrl(url: String): String { return when { url.contains("youtube.com/embed/") -> url.replace("/embed/", "/watch?v=").substringBefore("?") else -> url } } }