package com.streamflixreborn.streamflix.providers import android.util.Base64 import android.util.Log import com.streamflixreborn.streamflix.adapters.AppAdapter import com.streamflixreborn.streamflix.models.* import okhttp3.* import java.util.concurrent.TimeUnit object IptvSpainProvider : IptvProvider { override val name = "IPTV Spain" override val baseUrl = "https://iptv-org.github.io/iptv/languages/spa.m3u" override val logo = "https://i.ibb.co/SD7860Mv/IPTV-Spain-Canales.jpg" override val language = "es" private const val TAG = "IptvSpainProvider" private val OFFICIAL_CATEGORIES = listOf( "Animation", "Auto", "Business", "Classic", "Comedy", "Cooking", "Culture", "Documentary", "Education", "Entertainment", "Family", "General", "Interactive", "Kids", "Legislative", "Lifestyle", "Movies", "Music", "News", "Outdoor", "Public", "Relax", "Religious", "Science", "Series", "Shop", "Sports", "Travel", "Weather", "Undefined" ) private val client = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .cookieJar(object : CookieJar { private val cookieStore = mutableMapOf>() override fun saveFromResponse(url: HttpUrl, cookies: List) { cookieStore[url.host] = cookies } override fun loadForRequest(url: HttpUrl): List { return cookieStore[url.host] ?: listOf() } }) .addInterceptor { chain -> val request = chain.request().newBuilder() .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36") .build() chain.proceed(request) } .build() private var cachedChannels: List? = null private var lastFetchTime: Long = 0 private const val CACHE_DURATION = 30 * 60 * 1000 data class M3UChannel( val name: String, val url: String, val logo: String?, val group: String?, val userAgent: String? = null, val referrer: String? = null ) private fun createId(channel: M3UChannel): String { val rawId = "${channel.url}|${channel.name}|${channel.logo ?: ""}|${channel.userAgent ?: ""}|${channel.referrer ?: ""}" return Base64.encodeToString(rawId.toByteArray(), Base64.NO_WRAP) } private fun decodeId(id: String): Triple { if (id == "creador-info" || id == "apoyo-nando") { return Triple(id, "", "") } return try { val decoded = String(Base64.decode(id, Base64.DEFAULT)) val parts = decoded.split("|") Triple(parts[0], parts[1], parts.getOrNull(2) ?: "") } catch (e: Exception) { Triple(id, "Canal Desconocido", "") } } private fun getMetadataFromId(id: String): Map { return try { val decoded = String(Base64.decode(id, Base64.DEFAULT)) val parts = decoded.split("|") mapOf( "ua" to parts.getOrNull(3).takeIf { it?.isNotEmpty() == true }, "referer" to parts.getOrNull(4).takeIf { it?.isNotEmpty() == true } ) } catch (e: Exception) { emptyMap() } } private fun getAllChannels(): List { val now = System.currentTimeMillis() if (cachedChannels != null && (now - lastFetchTime) < CACHE_DURATION) return cachedChannels!! return try { val request = Request.Builder().url(baseUrl).build() val body = client.newCall(request).execute().body?.string() ?: return emptyList() val channels = parseM3U(body) cachedChannels = channels lastFetchTime = now channels } catch (e: Exception) { Log.e(TAG, "❌ Error M3U: ${e.message}") cachedChannels ?: emptyList() } } override suspend fun getHome(): List { val channels = getAllChannels() val homeGroups = listOf("News", "Sports", "Movies", "Entertainment", "Music", "General") val categories = mutableListOf() // 1. Procesamos categorías de canales primero val channelCategories = channels .filter { it.group != null && homeGroups.any { target -> it.group!!.contains(target, ignoreCase = true) } } .groupBy { channel -> homeGroups.find { channel.group!!.contains(it, ignoreCase = true) } ?: "General" } .map { (groupName, channelList) -> Category( name = groupName, list = channelList.distinctBy { it.name }.take(25).map { channel -> TvShow( id = createId(channel), title = channel.name, poster = channel.logo ?: "", banner = channel.logo ?: "" ) } ) }.sortedBy { it.name } categories.addAll(channelCategories) // 2. AÑADIMOS SOPORTE AL FINAL (PARTE ULTIMA) categories.add( Category( name = "Soporte y Ayuda", list = listOf( getInfoItem("creador-info"), getInfoItem("apoyo-nando") ) ) ) return categories } override suspend fun search(query: String, page: Int): List { if (page > 1) return emptyList() val allChannels = getAllChannels() val results = mutableListOf() val matchingGroups = OFFICIAL_CATEGORIES.filter { it.contains(query, ignoreCase = true) }.sorted() matchingGroups.forEach { results.add(Genre(id = it, name = "📂 Categoría: $it", shows = emptyList())) } val channelResults = allChannels.filter { it.name.contains(query, ignoreCase = true) }.distinctBy { it.name }.take(80).map { channel -> TvShow(id = createId(channel), title = channel.name, poster = channel.logo ?: "") } results.addAll(channelResults) return results } override suspend fun getGenre(id: String, page: Int): Genre { val groupChannels = getAllChannels().filter { it.group?.contains(id, ignoreCase = true) ?: false }.distinctBy { it.name } val pageSize = 40 val start = (page - 1) * pageSize val pagedList = groupChannels.drop(start).take(pageSize).map { channel -> TvShow(id = createId(channel), title = channel.name, poster = channel.logo ?: "") } return Genre(id = id, name = id, shows = pagedList) } override suspend fun getPeople( id: String, page: Int ): People { TODO("Not yet implemented") } override suspend fun getTvShow(id: String): TvShow { if (id == "creador-info" || id == "apoyo-nando") { return getInfoItem(id) } val (_, name, logo) = decodeId(id) return TvShow( id = id, title = name, poster = logo, banner = logo, overview = "Canal en Español: $name\nFuente: IPTV-Org", seasons = listOf(Season(id = id, number = 1, title = "Señal en Vivo")) ) } override suspend fun getEpisodesBySeason(seasonId: String): List { if (seasonId == "creador-info" || seasonId == "apoyo-nando") return emptyList() return listOf(Episode(id = seasonId, number = 1, title = "Reproducir Señal", season = null)) } override suspend fun getServers(id: String, videoType: Video.Type): List { if (id == "creador-info" || id == "apoyo-nando") return emptyList() return listOf(Video.Server(id = id, name = "Stream Directo")) } override suspend fun getVideo(server: Video.Server): Video { val (url, _, _) = decodeId(server.id) val meta = getMetadataFromId(server.id) Log.d(TAG, "🎬 Play: $url") meta["ua"]?.let { Log.d(TAG, "🛡️ Header UA: $it") } meta["referer"]?.let { Log.d(TAG, "🛡️ Header Referer: $it") } return Video(source = url, subtitles = emptyList()) } private fun getInfoItem(id: String): TvShow { val isReport = id == "creador-info" return TvShow( id = id, title = if (isReport) "Reportar problemas" else "Apoya al Proveedor", poster = if (isReport) "https://i.ibb.co/dsknGBHT/Imagen-de-Whats-App-2025-09-06-a-las-19-00-50-e8e5bcaa.jpg" else "https://i.ibb.co/B5gKLkqS/nuevo-formato-2-K-202604112205.jpg", banner = if (isReport) "https://i.ibb.co/dsknGBHT/Imagen-de-Whats-App-2025-09-06-a-las-19-00-50-e8e5bcaa.jpg" else "https://i.ibb.co/B5gKLkqS/nuevo-formato-2-K-202604112205.jpg", overview = if (isReport) { "Si algún canal no funciona o encuentras errores en el proveedor, por favor repórtalo en nuestro grupo oficial de Telegram." } else { "Si te gusta nuestro contenido y quieres ayudarnos a mantener los servidores activos, puedes realizar una donación voluntaria. ¡Gracias por tu apoyo!" }, seasons = emptyList() ) } private fun parseM3U(m3uRaw: String): List { val channels = mutableListOf() val lines = m3uRaw.lines() var curName = "" var curLogo = "" var curGroup = "" var curUA: String? = null var curRef: String? = null for (line in lines) { val t = line.trim() if (t.startsWith("#EXTINF")) { curName = t.substringAfterLast(",").trim() curLogo = Regex("""tvg-logo="([^"]+)"""").find(t)?.groupValues?.get(1) ?: "" curGroup = Regex("""group-title="([^"]+)"""").find(t)?.groupValues?.get(1) ?: "" curUA = Regex("""http-user-agent="([^"]+)"""").find(t)?.groupValues?.get(1) curRef = Regex("""http-referrer="([^"]+)"""").find(t)?.groupValues?.get(1) } else if (t.startsWith("#EXTVLCOPT:")) { if (t.contains("http-user-agent=")) curUA = t.substringAfter("http-user-agent=").trim() if (t.contains("http-referrer=")) curRef = t.substringAfter("http-referrer=").trim() } else if (t.startsWith("http")) { if (curName.isNotEmpty()) { channels.add(M3UChannel(curName, t, curLogo, curGroup, curUA, curRef)) curName = ""; curLogo = ""; curGroup = ""; curUA = null; curRef = null } } } return channels } override suspend fun getMovies(page: Int): List = emptyList() override suspend fun getTvShows(page: Int): List { val channels = getAllChannels() val pageSize = 50 val start = (page - 1) * pageSize if (start >= channels.size) return emptyList() return channels.drop(start).take(pageSize).map { channel -> TvShow(id = createId(channel), title = channel.name, poster = channel.logo ?: "") } } override suspend fun getMovie(id: String): Movie = Movie(id = id, title = "Live", poster = "") }