diff --git a/MaciptvProvider/build.gradle.kts b/MaciptvProvider/build.gradle.kts new file mode 100644 index 0000000..c27b28a --- /dev/null +++ b/MaciptvProvider/build.gradle.kts @@ -0,0 +1,29 @@ +dependencies { + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("com.google.android.material:material:1.4.0") + implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") +} + +// use an integer for version numbers +version = 1 + + +cloudstream { + // All of these properties are optional, you can safely remove them + + description = "Add your IPTV account or alternatively, the automatic account will allow you to watch live (Sport, movies, ...)." + authors = listOf("Eddy") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf("Live") + + iconUrl = "https://www.google.com/s2/favicons?domain=franceiptv.fr/&sz=%size%" + requiresResources = true +} \ No newline at end of file diff --git a/MaciptvProvider/src/main/AndroidManifest.xml b/MaciptvProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/MaciptvProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/MaciptvProvider/src/main/kotlin/com/lagradost/MacIPTVProvider.kt b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIPTVProvider.kt new file mode 100644 index 0000000..d359cbf --- /dev/null +++ b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIPTVProvider.kt @@ -0,0 +1,746 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import okhttp3.Interceptor +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.nicehttp.NiceResponse +import kotlinx.coroutines.runBlocking +import me.xdrop.fuzzywuzzy.FuzzySearch + + +class MacIPTVProvider(override var lang: String) : MainAPI() { + private val defaulmac_adresse = + "mac=00:1a:79:ae:2a:30" + private val defaultmainUrl = + "http://ultra-box.club/c/" + var defaultname = "BoxIPTV-MatrixOTT |${lang.uppercase()}|" + override var name = "Box Iptv |${lang.uppercase()}|" + override val hasQuickSearch = false + override val hasMainPage = true + override val supportedTypes = + setOf(TvType.Live) // live + + private var init = false + private var key: String? = "" + + companion object { + var companionName: String? = null + var loginMac: String? = null + var overrideUrl: String? = null + } + + private suspend fun getAuthHeader(): Map { + + mainUrl = overrideUrl.toString() + val main = mainUrl.uppercase().trim() + val localCredentials = loginMac + val mac = localCredentials?.uppercase()?.trim() + + if (main == "NONE" || main.isBlank() || mac == "NONE" || mac.isNullOrBlank()) { + mainUrl = defaultmainUrl + name = defaultname + + if (!init) { + val url_key = "$mainUrl/portal.php?type=stb&action=handshake&JsHttpRequest=1-xml" + val reponseGetkey = app.get( + url_key, headers = mapOf( + "Cookie" to defaulmac_adresse, + "User-Agent" to "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG200 stbapp ver: 2 rev: 250 Safari/533.3", + ) + ) + val keyJson = reponseGetkey.parsed() + key = keyJson.js?.token + } + init = true + return mapOf( + "Cookie" to defaulmac_adresse, + "User-Agent" to "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG200 stbapp ver: 2 rev: 250 Safari/533.3", + "Authorization" to "Bearer $key", + ) + } + name = (companionName ?: name) + " |${lang.uppercase()}|" + if (!init) { + val url_key = "$mainUrl/portal.php?type=stb&action=handshake&JsHttpRequest=1-xml" + val reponseGetkey = app.get( + url_key, headers = mapOf( + "Cookie" to "mac=$localCredentials", + "User-Agent" to "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG200 stbapp ver: 2 rev: 250 Safari/533.3", + ) + ) + val keyJson = reponseGetkey.parsed() + key = keyJson.js?.token + } + init = true + return mapOf( + "Cookie" to "mac=$localCredentials", + "User-Agent" to "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG200 stbapp ver: 2 rev: 250 Safari/533.3", + "Authorization" to "Bearer $key", + ) + } + + + private fun List.sortByname(query: String?): List { + return if (query == null) { + // Return list to base state if no query + this.sortedBy { it.title } + } else { + + this.sortedBy { + val name = cleanTitle(it.title) + -FuzzySearch.ratio(name.lowercase(), query.lowercase()) + } + } + } + + + /** + Cherche le site pour un titre spécifique + + La recherche retourne une SearchResponse, qui peut être des classes suivants: AnimeSearchResponse, MovieSearchResponse, TorrentSearchResponse, TvSeriesSearchResponse + Chaque classes nécessite des données différentes, mais a en commun le nom, le poster et l'url + **/ + private val resultsSearchNbr = 50 + override suspend fun search(query: String): List { + val searchResutls = ArrayList() + arraymediaPlaylist.sortByname(query).take(resultsSearchNbr).forEach { media -> + searchResutls.add( + LiveSearchResponse( + media.title, + media.url, + name, + TvType.Live, + media.url_image, + ) + ) + } + + return searchResutls + + } + + data class Root_epg( + + @JsonProperty("js") var js: ArrayList = arrayListOf() + + ) + + data class Js_epg( + + /* @JsonProperty("id") var id: String? = null, + @JsonProperty("ch_id") var chId: String? = null, + @JsonProperty("correct") var correct: String? = null, + @JsonProperty("time") var time: String? = null, + @JsonProperty("time_to") var timeTo: String? = null, + @JsonProperty("duration") var duration: Int? = null,*/ + @JsonProperty("name") var name: String? = null, + @JsonProperty("descr") var descr: String? = null, +/* @JsonProperty("real_id") var realId: String? = null, + @JsonProperty("category") var category: String? = null, + @JsonProperty("director") var director: String? = null, + @JsonProperty("actor") var actor: String? = null, + @JsonProperty("start_timestamp") var startTimestamp: Int? = null, + @JsonProperty("stop_timestamp") var stopTimestamp: Int? = null,*/ + @JsonProperty("t_time") var tTime: String? = null, + @JsonProperty("t_time_to") var tTimeTo: String? = null, + /* @JsonProperty("mark_memo") var markMemo: Int? = null, + @JsonProperty("mark_archive") var markArchive: Int? = null*/ + + ) + + private fun getEpg(response: String): String { + val reponseJSON_0 = tryParseJson(response) + var description = "" + reponseJSON_0?.js?.forEach { epg_i -> + val name = epg_i.name + val descr = epg_i.descr + val t_time = epg_i.tTime + val t_time_to = epg_i.tTimeTo + val new_descr = "||($t_time -> $t_time_to) $name : $descr" + if (!description.contains(new_descr)) { + description = "$description\n $new_descr" + } + + } + return description + } + + + + override suspend fun load(url: String): LoadResponse { + + var link = "" + var title = "" + var posterUrl = "" + var description = "" + val header = getAuthHeader() + val allresultshome: MutableList = mutableListOf() + for (media in arraymediaPlaylist) { + val keyId = "/-${media.id}-" + if (url.contains(keyId) || url == media.url) { + val epg_url = + "$mainUrl/portal.php?type=itv&action=get_short_epg&ch_id=${media.ch_id}&size=10&JsHttpRequest=1-xml" // descriptif + val response = app.get(epg_url, headers = header) + description = getEpg(response.text) + link = media.url + title = media.title + val a = cleanTitle(title) + posterUrl = media.url_image.toString() + var b_new: String + arraymediaPlaylist.forEach { channel -> + val b = cleanTitle(channel.title) + b_new = b.take(6) + if (channel.id != media.id && a.take(6) + .contains(b_new) && media.tv_genre_id == channel.tv_genre_id + ) { + val streamurl = channel.url + val channelname = channel.title + val posterurl = channel.url_image.toString() + val uppername = channelname.uppercase() + val quality = getQualityFromString( + when (!channelname.isNullOrBlank()) { + uppername.contains(findKeyWord("UHD")) -> { + "UHD" + } + uppername.contains(findKeyWord("HD")) -> { + "HD" + } + uppername.contains(findKeyWord("SD")) -> { + "SD" + } + uppername.contains(findKeyWord("FHD")) -> { + "HD" + } + uppername.contains(findKeyWord("4K")) -> { + "FourK" + } + + else -> { + null + } + } + ) + allresultshome.add( + LiveSearchResponse( + name = cleanTitleButKeepNumber(channelname), + url = streamurl, + name, + TvType.Live, + posterUrl = posterurl, + quality = quality, + ) + ) + + } + + } + + break + } + + } + + if (allresultshome.size >= 1) { + val recommendation = allresultshome + return LiveStreamLoadResponse( + name = title, + url = link, + apiName = this.name, + dataUrl = link, + posterUrl = posterUrl, + plot = description, + recommendations = recommendation + ) + } else { + return LiveStreamLoadResponse( + name = title, + url = link, + apiName = this.name, + dataUrl = link, + posterUrl = posterUrl, + plot = description, + ) + } + } + + /** get the channel id */ + val regexCode_ch = Regex("""\/(\d*)\?""") + + /** + * Use new token. + * */ + override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor { + // Needs to be object instead of lambda to make it compile correctly + return object : Interceptor { + override fun intercept(chain: Interceptor.Chain): okhttp3.Response { + val request = chain.request() + if (request.url.toString().contains("token") + + ) { + val chID = regexCode_ch.find(request.url.toString())!!.groupValues[1] + "_" + val TokenLink = + "$mainUrl/portal.php?type=itv&action=create_link&cmd=ffmpeg%20http://localhost/ch/$chID&series=&forced_storage=0&disable_ad=0&download=0&force_ch_link_check=0&JsHttpRequest=1-xml" + + var link: String + var lien: String + runBlocking { + val header = getAuthHeader() + val getTokenLink = app.get(TokenLink, headers = header).text + val regexGetLink = Regex("""(http.*)\"\},""") + link = regexGetLink.find(getTokenLink)?.groupValues?.get(1).toString() + .replace( + """\""", + "" + ) + lien = link + if (link.contains("extension")) { + val headerlocation = app.get( + link, + allowRedirects = false + ).headers + val redirectlink = headerlocation.get("location") + .toString() + if (redirectlink != "null") { + lien = redirectlink + } + } + } + + val newRequest = chain.request() + .newBuilder().url(lien).build() + return chain.proceed(newRequest) + } else { + return chain.proceed(chain.request()) + } + } + } + } + + /** récupere les liens .mp4 ou m3u8 directement à partir du paramètre data généré avec la fonction load()**/ + override suspend fun loadLinks( + data: String, // fournit par load() + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ): Boolean { + + val header = getAuthHeader() + val TokenLink = + "$mainUrl/portal.php?type=itv&action=create_link&cmd=ffmpeg%20$data&series=&forced_storage=0&disable_ad=0&download=0&force_ch_link_check=0&JsHttpRequest=1-xml" + val getTokenLink = app.get(TokenLink, headers = header).text + val regexGetLink = Regex("""(http.*)\"\},""") + val link = + regexGetLink.find(getTokenLink)?.groupValues?.get(1).toString().replace("""\""", "") + + + val head = mapOf( + "Accept" to "*/*", + "Accept-Language" to "en_US", + "User-Agent" to "VLC/3.0.18 LibVLC/3.0.18", + "Range" to "bytes=0-" + ) + + + var lien = link + if (link.contains("extension")) { + val headerlocation = app.get( + link, + allowRedirects = false + ).headers + val redirectlink = headerlocation.get("location") + .toString() + + if (redirectlink != "null") { + lien = redirectlink + } + } + val isM3u8 = false// lien.contains("extension=ts") + callback.invoke( + ExtractorLink( + name, + name, + lien, + mainUrl, + Qualities.Unknown.value, + isM3u8 = isM3u8, + headers = head + ) + ) + + return true + } + + + private val arraymediaPlaylist = ArrayList() + + data class Channel( + var title: String, + var url: String, + val url_image: String?, + val lang: String?, + var id: String?, + var tv_genre_id: String?, + var ch_id: String?, + ) + + data class Cmds( + + // @JsonProperty("id") var id: String? = null, + @JsonProperty("ch_id") var chId: String? = null, + /* @JsonProperty("priority") var priority: String? = null, + @JsonProperty("url") var url: String? = null, + @JsonProperty("status") var status: String? = null, + @JsonProperty("use_http_tmp_link") var useHttpTmpLink: String? = null, + @JsonProperty("wowza_tmp_link") var wowzaTmpLink: String? = null, + @JsonProperty("user_agent_filter") var userAgentFilter: String? = null, + @JsonProperty("use_load_balancing") var useLoadBalancing: String? = null, + @JsonProperty("changed") var changed: String? = null, + @JsonProperty("enable_monitoring") var enableMonitoring: String? = null, + @JsonProperty("enable_balancer_monitoring") var enableBalancerMonitoring: String? = null, + @JsonProperty("nginx_secure_link") var nginxSecureLink: String? = null, + @JsonProperty("flussonic_tmp_link") var flussonicTmpLink: String? = null*/ + + ) + + data class Data( + + @JsonProperty("id") var id: String? = null, + @JsonProperty("name") var name: String? = null, + @JsonProperty("number") var number: String? = null, +/* @JsonProperty("censored") var censored: Int? = null, + @JsonProperty("cmd") var cmd: String? = null, + @JsonProperty("cost") var cost: String? = null, + @JsonProperty("count") var count: String? = null, + @JsonProperty("status") var status: Int? = null, + @JsonProperty("hd") var hd: String? = null,*/ + @JsonProperty("tv_genre_id") var tvGenreId: String? = null, +/* @JsonProperty("base_ch") var baseCh: String? = null, + @JsonProperty("xmltv_id") var xmltvId: String? = null, + @JsonProperty("service_id") var serviceId: String? = null, + @JsonProperty("bonus_ch") var bonusCh: String? = null, + @JsonProperty("volume_correction") var volumeCorrection: String? = null, + @JsonProperty("mc_cmd") var mcCmd: String? = null, + @JsonProperty("enable_tv_archive") var enableTvArchive: Int? = null, + @JsonProperty("wowza_tmp_link") var wowzaTmpLink: String? = null, + @JsonProperty("wowza_dvr") var wowzaDvr: String? = null, + @JsonProperty("use_http_tmp_link") var useHttpTmpLink: String? = null, + @JsonProperty("monitoring_status") var monitoringStatus: String? = null, + @JsonProperty("enable_monitoring") var enableMonitoring: String? = null, + @JsonProperty("enable_wowza_load_balancing") var enableWowzaLoadBalancing: String? = null, + @JsonProperty("cmd_1") var cmd1: String? = null, + @JsonProperty("cmd_2") var cmd2: String? = null, + @JsonProperty("cmd_3") var cmd3: String? = null,*/ + @JsonProperty("logo") var logo: String? = null, + /* @JsonProperty("correct_time") var correctTime: String? = null, + @JsonProperty("nimble_dvr") var nimbleDvr: String? = null, + @JsonProperty("allow_pvr") var allowPvr: Int? = null, + @JsonProperty("allow_local_pvr") var allowLocalPvr: Int? = null, + @JsonProperty("allow_remote_pvr") var allowRemotePvr: Int? = null, + @JsonProperty("modified") var modified: String? = null, + @JsonProperty("allow_local_timeshift") var allowLocalTimeshift: String? = null, + @JsonProperty("nginx_secure_link") var nginxSecureLink: String? = null, + @JsonProperty("tv_archive_duration") var tvArchiveDuration: Int? = null, + @JsonProperty("locked") var locked: Int? = null, + @JsonProperty("lock") var lock: Int? = null, + @JsonProperty("fav") var fav: Int? = null, + @JsonProperty("archive") var archive: Int? = null, + @JsonProperty("genres_str") var genresStr: String? = null, + @JsonProperty("cur_playing") var curPlaying: String? = null, + @JsonProperty("epg") var epg: ArrayList = arrayListOf(), + @JsonProperty("open") var open: Int? = null,*/ + @JsonProperty("cmds") var cmds: ArrayList = arrayListOf(), + /* @JsonProperty("use_load_balancing") var useLoadBalancing: Int? = null, + @JsonProperty("pvr") var pvr: Int? = null*/ + + ) + + data class JsKey( + + @JsonProperty("token") var token: String? = null + + ) + + data class Getkey( + + @JsonProperty("js") var js: JsKey? = JsKey() + + ) + + data class JsInfo( + + @JsonProperty("mac") var mac: String? = null, + @JsonProperty("phone") var phone: String? = null + + ) + + data class GetExpiration( + + @JsonProperty("js") var js: JsInfo? = JsInfo() + + ) + + data class Js( + + @JsonProperty("total_items") var totalItems: Int? = null, + @JsonProperty("max_page_items") var maxPageItems: Int? = null, + /* @JsonProperty("selected_item") var selectedItem: Int? = null, + @JsonProperty("cur_page") var curPage: Int? = null,*/ + @JsonProperty("data") var data: ArrayList = arrayListOf() + + ) + + data class JsonGetGenre( + + @JsonProperty("js") var js: ArrayList = arrayListOf() + + ) + + data class Js_category( + + @JsonProperty("id") var id: String? = null, + @JsonProperty("title") var title: String? = null, + /* @JsonProperty("alias") var alias: String? = null, + @JsonProperty("active_sub") var activeSub: Boolean? = null, + @JsonProperty("censored") var censored: Int? = null*/ + + ) + + data class Root( + + @JsonProperty("js") var js: Js? = Js() + + ) + + private var codeCountry = lang + private fun findKeyWord(str: String): Regex { + val upperSTR = str.uppercase() + val sequence = when (true) { + (upperSTR == "EN") -> { + "US|UK" + } + else -> upperSTR + } + return """(?:^|\W+|\s)+($sequence)(?:\s|\W+|${'$'}|\|)+""".toRegex() + } + + private fun String.isContainsTargetCountry(): Boolean { + val getLang = lang.uppercase() + var resp = false + when (true) { + (getLang == "FR") -> { + arrayListOf("FRENCH", "FRANCE").forEach { + if (this.uppercase().contains(findKeyWord(it))) { + resp = true + } + } + } + (getLang == "EN") -> { + arrayListOf("ENGLISH", "USA").forEach { + if (this.uppercase().contains(findKeyWord(it))) { + resp = true + } + } + } + (getLang == "AR") -> { + arrayListOf("ARABIC", "ARAB", "ARABIA").forEach { + if (this.uppercase().contains(findKeyWord(it))) { + resp = true + } + } + } + else -> resp = false + } + + + + return resp + } + + private fun cleanTitle(title: String): String { + return title.uppercase().replace("""(\s\d{1,}${'$'}|\s\d{1,}\s)""".toRegex(), " ") + .replace("""FHD""", "") + .replace(findKeyWord("VIP"), "") + .replace("""UHD""", "").replace(rgxcodeCountry, "").replace("""HEVC""", "") + .replace("""HDR""", "").replace("""SD""", "").replace("""4K""", "") + .replace("""HD""", "").replace(rgxcodeCountry, "").trim() + } + + + private fun getFlag(sequence: String): String { + val FR = findKeyWord("FR|FRANCE|FRENCH") + val US = findKeyWord("US|USA") + val AR = findKeyWord("AR|ARAB|ARABIC|ARABIA") + val UK = findKeyWord("UK") + val flag: String + flag = when (true) { + sequence.uppercase() + .contains(FR) -> "\uD83C\uDDE8\uD83C\uDDF5" + sequence.uppercase() + .contains(US) -> "\uD83C\uDDFA\uD83C\uDDF8" + sequence.uppercase() + .contains(UK) -> "\uD83C\uDDEC\uD83C\uDDE7" + sequence.uppercase() + .contains(AR) -> " نظرة" + else -> "" + } + return flag + } + + + val rgxcodeCountry = findKeyWord(codeCountry) + val arrayHomepage = arrayListOf() + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + + if (!init) { + val header = getAuthHeader() + val url_info = + "$mainUrl/portal.php?type=account_info&action=get_main_info&JsHttpRequest=1-xml" + val urlGetGenre = + "$mainUrl/portal.php?type=itv&action=get_genres&JsHttpRequest=1-xml" + val urlGetallchannels = + "$mainUrl/portal.php?type=itv&action=get_all_channels&JsHttpRequest=1-xml" + + var reponseGetInfo: NiceResponse? = null + var responseGetgenre: NiceResponse? = null + var responseAllchannels: NiceResponse? = null + listOf( + url_info, + urlGetGenre, + urlGetallchannels + ).apmap { url -> + val response = app.get(url, headers = header) + when (true) { + url.contains("action=get_main_info") -> { + reponseGetInfo = response + } + url.contains("action=get_genre") -> { + responseGetgenre = response + } + url.contains("action=get_all_channels") -> { + responseAllchannels = response + } + else -> { + "" + } + } + } + ///////////// GET EXPIRATION + val infoExpirationJson = reponseGetInfo!!.parsed() + val expiration = infoExpirationJson.js?.phone + ////////////////////////// GET ALL GENRES + val responseGetGenretoJSON = responseGetgenre!!.parsed() + ////////////////////////// GET ALL CHANNELS + val responseAllchannelstoJSON = responseAllchannels!!.parsed() + val AllchannelstoJSON = responseAllchannelstoJSON.js!!.data.sortByTitleNumber() + var firstCat = true + responseGetGenretoJSON.js.forEach { js -> + val idGenre = js.id + val categoryTitle = js.title.toString() + + if (idGenre!!.contains("""\d""".toRegex()) && (categoryTitle.uppercase() + .contains(rgxcodeCountry) || + categoryTitle.isContainsTargetCountry() + ) + ) { + AllchannelstoJSON.forEach { data -> + val genre = data.tvGenreId + + if (genre != null) { + if (genre == idGenre) { + val name = data.name.toString() + val tv_genre_id = data.tvGenreId + val idCH = data.id + val link = "http://localhost/ch/$idCH" + "_" + val logo = data.logo?.replace("""\""", "") + val ch_id = data.cmds[0].chId + arraymediaPlaylist.add( + Channel( + name, + link, + logo, + "", + idCH, + tv_genre_id, + ch_id + ) + ) + + } + } + } + } + /***************************************** */ + + val groupMedia = ArrayList() + var b_new: String + var newgroupMedia: Boolean + val home = arraymediaPlaylist.mapNotNull { media -> + val b = cleanTitle(media.title)// + b_new = b.take(6) + newgroupMedia = true + for (nameMedia in groupMedia) { + if (nameMedia.contains(b_new) && (media.tv_genre_id == idGenre)) { + newgroupMedia = false + break + } + } + groupMedia.contains(b_new) + if (page == 1 && (media.tv_genre_id == idGenre) && newgroupMedia + ) { + groupMedia.add(b_new) + val groupName = cleanTitle(media.title) + + LiveSearchResponse( + groupName, + "$mainUrl/-${media.id}-", + name, + TvType.Live, + media.url_image, + ) + } else { + null + } + } + val flag: String + if ((categoryTitle.uppercase() + .contains(rgxcodeCountry) || categoryTitle.isContainsTargetCountry()) + ) { + + + flag = getFlag(categoryTitle) + val nameGenre = if (firstCat) { + firstCat = false + "$flag ${cleanTitle(categoryTitle)} \uD83D\uDCFA $name \uD83D\uDCFA (⏳ $expiration)" + } else { + "$flag ${cleanTitle(categoryTitle)}" + } + arrayHomepage.add(HomePageList(nameGenre, home, isHorizontalImages = true)) + } + + } + + } + + return HomePageResponse( + arrayHomepage + ) + } + + private fun cleanTitleButKeepNumber(title: String): String { + return title.uppercase().replace("""FHD""", "") + .replace(findKeyWord("VIP"), "") + .replace("""UHD""", "").replace("""HEVC""", "") + .replace("""HDR""", "").replace("""SD""", "").replace("""4K""", "") + .replace("""HD""", "").replace(rgxcodeCountry, "").trim() + } + + private fun ArrayList.sortByTitleNumber(): ArrayList { + val regxNbr = Regex("""(\s\d{1,}${'$'}|\s\d{1,}\s)""") + return ArrayList(this.sortedBy { + val str = it.name.toString() + regxNbr.find(str)?.groupValues?.get(0)?.trim()?.toInt() ?: 1000 + }) + } +} \ No newline at end of file diff --git a/MaciptvProvider/src/main/kotlin/com/lagradost/MacIPTVProviderPlugin.kt b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIPTVProviderPlugin.kt new file mode 100644 index 0000000..b870dd6 --- /dev/null +++ b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIPTVProviderPlugin.kt @@ -0,0 +1,37 @@ + +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import com.lagradost.cloudstream3.AcraApplication.Companion.context +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity +import com.lagradost.cloudstream3.ui.settings.SettingsAccount + +@CloudstreamPlugin +class MacIPTVProviderPlugin : Plugin() { + val iptvboxApi = MacIptvAPI(0) + + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + iptvboxApi.init() + registerMainAPI(MacIPTVProvider("fr")) + registerMainAPI(MacIPTVProvider("en")) + registerMainAPI(MacIPTVProvider("ar")) + ioSafe { + iptvboxApi.initialize() + } + } + + init { + this.openSettings = { + val activity = it as? AppCompatActivity + if (activity != null) { + val frag = MacIptvSettingsFragment(this, iptvboxApi) + frag.show(activity.supportFragmentManager, iptvboxApi.name) + } + } + } +} \ No newline at end of file diff --git a/MaciptvProvider/src/main/kotlin/com/lagradost/MacIptvAPI.kt b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIptvAPI.kt new file mode 100644 index 0000000..b995e62 --- /dev/null +++ b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIptvAPI.kt @@ -0,0 +1,64 @@ +package com.lagradost + +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager + +class MacIptvAPI(index: Int) : InAppAuthAPIManager(index) { + override val name = "Iptvbox" + override val idPrefix = "iptvbox" + override val icon = R.drawable.ic_baseline_extension_24 + override val requiresUsername = true + override val requiresPassword = true + override val requiresServer = true + override val createAccountUrl = "" + + companion object { + const val IPTVBOX_USER_KEY: String = "iptvbox_user" + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + return getKey(accountId, IPTVBOX_USER_KEY) + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + val data = getLatestLoginData() ?: return null + return AuthAPI.LoginInfo(name = data.username ?: data.server, accountIndex = accountIndex) + } + + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + if (data.server.isNullOrBlank()) return false // we require a server + switchToNewAccount() + setKey(accountId, IPTVBOX_USER_KEY, data) + registerAccount() + initialize() + AccountManager.inAppAuths + + return true + } + + override fun logOut() { + removeAccountKeys() + initializeData() + } + + private fun initializeData() { + val data = getLatestLoginData() ?: run { + MacIPTVProvider.overrideUrl = null + MacIPTVProvider.loginMac = null + MacIPTVProvider.companionName = null + return + } + MacIPTVProvider.overrideUrl = data.server?.removeSuffix("/") + MacIPTVProvider.loginMac = data.password ?: "" + MacIPTVProvider.companionName = data.username + } + + override suspend fun initialize() { + initializeData() + } +} \ No newline at end of file diff --git a/MaciptvProvider/src/main/kotlin/com/lagradost/MacIptvSettingsFragment.kt b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIptvSettingsFragment.kt new file mode 100644 index 0000000..98c0270 --- /dev/null +++ b/MaciptvProvider/src/main/kotlin/com/lagradost/MacIptvSettingsFragment.kt @@ -0,0 +1,93 @@ +package com.lagradost + +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.lagradost.cloudstream3.R +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.plugins.Plugin +import com.lagradost.cloudstream3.ui.settings.SettingsAccount.Companion.showLoginInfo +import com.lagradost.cloudstream3.ui.settings.SettingsAccount.Companion.addAccount +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + + +class MacIptvSettingsFragment(private val plugin: Plugin, val maciptvAPI: MacIptvAPI) : + BottomSheetDialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val id = plugin.resources!!.getIdentifier("nginx_settings", "layout", BuildConfig.LIBRARY_PACKAGE_NAME) + val layout = plugin.resources!!.getLayout(id) + return inflater.inflate(layout, container, false) + } + + private fun View.findView(name: String): T { + val id = plugin.resources!!.getIdentifier(name, "id", BuildConfig.LIBRARY_PACKAGE_NAME) + return this.findViewById(id) + } + + private fun getDrawable(name: String): Drawable? { + val id = plugin.resources!!.getIdentifier(name, "drawable", BuildConfig.LIBRARY_PACKAGE_NAME) + return ResourcesCompat.getDrawable(plugin.resources!!, id, null) + } + + private fun getString(name: String): String? { + val id = plugin.resources!!.getIdentifier(name, "string", BuildConfig.LIBRARY_PACKAGE_NAME) + return plugin.resources!!.getString(id) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val infoView = view.findView("nginx_info") + val infoTextView = view.findView("info_main_text") + val infoSubTextView = view.findView("info_sub_text") + val infoImageView = view.findView("nginx_info_imageview") + + infoTextView.text = getString("nginx_info_title") ?: "Nginx" + infoSubTextView.text = getString("nginx_info_summary") ?: "" + infoImageView.setImageDrawable(getDrawable("nginx_question")) + infoImageView.imageTintList = + ColorStateList.valueOf(view.context.colorFromAttribute(R.attr.white)) + + val loginView = view.findView("nginx_login") + val loginTextView = view.findView("main_text") + val loginImageView = view.findView("nginx_login_imageview") + loginImageView.setImageDrawable(getDrawable("nginx")) + loginImageView.imageTintList = + ColorStateList.valueOf(view.context.colorFromAttribute(R.attr.white)) + + // object : View.OnClickListener is required to make it compile because otherwise it used invoke-customs + infoView.setOnClickListener(object : View.OnClickListener { + override fun onClick(v: View?) { + openBrowser(maciptvAPI.createAccountUrl) + } + }) + + + loginTextView.text = view.context.resources.getString(R.string.login_format).format( + maciptvAPI.name, + view.context.resources.getString(R.string.account)) + + loginView.setOnClickListener(object : View.OnClickListener { + override fun onClick(v: View?) { + val info = maciptvAPI.loginInfo() + if (info != null) { + showLoginInfo(activity, maciptvAPI, info) + } else { + addAccount(activity, maciptvAPI) + } + } + }) + } +} \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/drawable/nginx.xml b/MaciptvProvider/src/main/res/drawable/nginx.xml new file mode 100644 index 0000000..1e7d7ac --- /dev/null +++ b/MaciptvProvider/src/main/res/drawable/nginx.xml @@ -0,0 +1,12 @@ + + + diff --git a/MaciptvProvider/src/main/res/drawable/nginx_question.xml b/MaciptvProvider/src/main/res/drawable/nginx_question.xml new file mode 100644 index 0000000..747ffb0 --- /dev/null +++ b/MaciptvProvider/src/main/res/drawable/nginx_question.xml @@ -0,0 +1,17 @@ + + + + diff --git a/MaciptvProvider/src/main/res/layout/nginx_settings.xml b/MaciptvProvider/src/main/res/layout/nginx_settings.xml new file mode 100644 index 0000000..b1c4bd8 --- /dev/null +++ b/MaciptvProvider/src/main/res/layout/nginx_settings.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-bp/strings.xml b/MaciptvProvider/src/main/res/values-bp/strings.xml new file mode 100644 index 0000000..3e974cf --- /dev/null +++ b/MaciptvProvider/src/main/res/values-bp/strings.xml @@ -0,0 +1,5 @@ + + + O que é Nginx ? + Nginx é um software que pode ser usado para exibir arquivos de um servidor que você possui. Clique para ver um guia de configuração do Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-cs/strings.xml b/MaciptvProvider/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000..513a466 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-cs/strings.xml @@ -0,0 +1,5 @@ + + + Co je to Nginx? + Nginx je software, který může být použit ke zobrazení souborů na serveru, který vlastníte. Klikněte pro zobrazení návodu k nastavení Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-es/strings.xml b/MaciptvProvider/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..604a20b --- /dev/null +++ b/MaciptvProvider/src/main/res/values-es/strings.xml @@ -0,0 +1,5 @@ + + + ¿Qué es Nginx? + Nginx es un software que se puede usar para mostrar archivos de un servidor de su propiedad. Pulse para ver una guía de configuración de Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-fr/strings.xml b/MaciptvProvider/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..3cd3362 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-fr/strings.xml @@ -0,0 +1,9 @@ + + + C\'est quoi Maciptv ? + NomIPTV + Maciptv est un emulateur qui va vous permettre de profiter de vos comptes IPTV. Donner l\'url portail du fournisseur http:\/\/exemple.com\/c\/ et votre mac 00:1A:79:XX:XX:XX comme password123. Pour sélectionner/revenir au compte par défaut, créer un compte avec \'none\' à la place de \'127.0.0.1\' et pas besoin de renseigner les autres infos + Addresse du serveur IPTV + 00:1A:79:XX:XX + http:\/\/exemple\-portal.com:8080\/c\/ + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-in/strings.xml b/MaciptvProvider/src/main/res/values-in/strings.xml new file mode 100644 index 0000000..3685d63 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-in/strings.xml @@ -0,0 +1,5 @@ + + + Apa itu Nginx ? + Nginx adalah sebuah software yang dapat digunakan untuk menampilkan file dari server yang anda miliki. Klik untuk melihat panduan pengaturan Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-it/strings.xml b/MaciptvProvider/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..435f963 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-it/strings.xml @@ -0,0 +1,5 @@ + + + Cos\'è Nginx? + Nginx è un software che può essere utilizzato per visualizzare i file da un server di proprietà. Fare clic per vedere la guida all\'installazione di Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-nl/strings.xml b/MaciptvProvider/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..1cf37c2 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-nl/strings.xml @@ -0,0 +1,5 @@ + + + Wat is Nginx ? + Nginx is een software die kan worden gebruikt om bestanden weer te geven van een server die u bezit. Klik om een Nginx installatiegids te zien + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-pl/strings.xml b/MaciptvProvider/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..0808836 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-pl/strings.xml @@ -0,0 +1,5 @@ + + + Co to Nginx? + Nginx to oprogramowanie, które może być używane do wyświetlania plików z serwera, którego jesteś właścicielem. Kliknij, aby zobaczyć przewodnik konfiguracji Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-ro/strings.xml b/MaciptvProvider/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000..d44ede0 --- /dev/null +++ b/MaciptvProvider/src/main/res/values-ro/strings.xml @@ -0,0 +1,5 @@ + + + Ce este Nginx? + Nginx este un software care poate fi utilizat pentru a vizualiza fișiere de pe un server proprietar. Faceți clic pentru a vedea ghidul de instalare Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-tr/strings.xml b/MaciptvProvider/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..ea0fb7a --- /dev/null +++ b/MaciptvProvider/src/main/res/values-tr/strings.xml @@ -0,0 +1,5 @@ + + + Nginx nedir? + Nginx, sahip olduğunuz bir sunucudan dosyaları görüntülemek için kullanılabilecek bir yazılımdır. Nginx kurulum kılavuzunu görmek için tıklayın + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values-vi/strings.xml b/MaciptvProvider/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000..0852a2f --- /dev/null +++ b/MaciptvProvider/src/main/res/values-vi/strings.xml @@ -0,0 +1,5 @@ + + + Nginx là gì? + Nginx được dùng để hiển thị các tệp từ máy chủ mà bạn sở hữu. Chọn để xem hướng dẫn thiết lập Nginx + \ No newline at end of file diff --git a/MaciptvProvider/src/main/res/values/strings.xml b/MaciptvProvider/src/main/res/values/strings.xml new file mode 100644 index 0000000..6c1cc7f --- /dev/null +++ b/MaciptvProvider/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + What is Maciptv ? + 00:1A:79:XX:XX + http:\/\/exemple\-portal.com:8080\/c\/ + MyCoolIPTVname + Maciptv is an extension that will allow you to enjoy your IPTV accounts. Give the provider\'s portal url http:\/\/exemple.com\/c\/ and your mac 00:1A:79:XX:XX as password123. To select/revert to the default account, create an account with \'none\' instead of \'127.0.0.1\' and no need to fill in the other info + \ No newline at end of file