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