From e3e62c74f67ece8c918eeb0b27ee5aeaddc7fb68 Mon Sep 17 00:00:00 2001
From: antonydp <38143733+antonydp@users.noreply.github.com>
Date: Sat, 20 Aug 2022 12:04:51 +0200
Subject: [PATCH] github.com/Free-TV/IPTV and github.com/iptv-org/iptv
providers (#1)
---
FreeTVProvider/build.gradle.kts | 24 ++
FreeTVProvider/src/main/AndroidManifest.xml | 2 +
.../kotlin/com/lagradost/FreeTVProvider.kt | 343 ++++++++++++++++
.../com/lagradost/FreeTVProviderPlugin.kt | 14 +
IptvorgProvider/build.gradle.kts | 24 ++
IptvorgProvider/src/main/AndroidManifest.xml | 2 +
.../kotlin/com/lagradost/IptvorgProvider.kt | 367 ++++++++++++++++++
.../com/lagradost/IptvorgProviderPlugin.kt | 14 +
8 files changed, 790 insertions(+)
create mode 100644 FreeTVProvider/build.gradle.kts
create mode 100644 FreeTVProvider/src/main/AndroidManifest.xml
create mode 100644 FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProvider.kt
create mode 100644 FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProviderPlugin.kt
create mode 100644 IptvorgProvider/build.gradle.kts
create mode 100644 IptvorgProvider/src/main/AndroidManifest.xml
create mode 100644 IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProvider.kt
create mode 100644 IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProviderPlugin.kt
diff --git a/FreeTVProvider/build.gradle.kts b/FreeTVProvider/build.gradle.kts
new file mode 100644
index 0000000..9306c22
--- /dev/null
+++ b/FreeTVProvider/build.gradle.kts
@@ -0,0 +1,24 @@
+// use an integer for version numbers
+version = 1
+
+
+cloudstream {
+ // All of these properties are optional, you can safely remove them
+
+ // description = "Lorem Ipsum"
+ authors = listOf("Adippe")
+
+ /**
+ * 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=github.com&sz=%size%"
+}
diff --git a/FreeTVProvider/src/main/AndroidManifest.xml b/FreeTVProvider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29aec9d
--- /dev/null
+++ b/FreeTVProvider/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProvider.kt b/FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProvider.kt
new file mode 100644
index 0000000..8b7b3e9
--- /dev/null
+++ b/FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProvider.kt
@@ -0,0 +1,343 @@
+package com.lagradost
+
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import java.io.InputStream
+
+class FreeTVProvider : MainAPI() {
+ override var lang = "en"
+ override var mainUrl = "https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8"
+ override var name = "Free-TV"
+ override val hasMainPage = true
+ override val hasChromecastSupport = true
+ override val supportedTypes = setOf(
+ TvType.Live,
+ )
+
+ override suspend fun getMainPage(
+ page: Int,
+ request : MainPageRequest
+ ): HomePageResponse {
+ val data = IptvPlaylistParser().parseM3U(app.get(mainUrl).text)
+ return HomePageResponse(data.items.groupBy{it.attributes["group-title"]}.map { group ->
+ val title = group.key ?: ""
+ val show = group.value.map { channel ->
+ val streamurl = channel.url.toString()
+ val channelname = channel.title.toString()
+ val posterurl = channel.attributes["tvg-logo"].toString()
+ val nation = channel.attributes["group-title"].toString()
+ LiveSearchResponse(
+ channelname,
+ LoadData(streamurl, channelname, posterurl, nation).toJson(),
+ this@FreeTVProvider.name,
+ TvType.Live,
+ posterurl,
+ lang = channel.attributes["group-title"]
+ )
+ }
+ HomePageList(
+ title,
+ show,
+ isHorizontalImages = true
+ )
+ })
+ }
+
+ override suspend fun search(query: String): List {
+ val data = IptvPlaylistParser().parseM3U(app.get(mainUrl).text)
+
+ return data.items.filter { it.attributes["tvg-id"]?.contains(query) ?: false }.map { channel ->
+ val streamurl = channel.url.toString()
+ val channelname = channel.attributes["tvg-id"].toString()
+ val posterurl = channel.attributes["tvg-logo"].toString()
+ val nation = channel.attributes["group-title"].toString()
+ LiveSearchResponse(
+ channelname,
+ LoadData(streamurl, channelname, posterurl, nation).toJson(),
+ this@FreeTVProvider.name,
+ TvType.Live,
+ posterurl,
+ )
+ }
+ }
+
+ override suspend fun load(url: String): LoadResponse {
+ val data = parseJson(url)
+ return LiveStreamLoadResponse(
+ data.title,
+ data.url,
+ this.name,
+ url,
+ data.poster,
+ plot = data.nation
+ )
+ }
+ data class LoadData(
+ val url: String,
+ val title: String,
+ val poster: String,
+ val nation: String
+
+ )
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ val loadData = parseJson(data)
+ callback.invoke(
+ ExtractorLink(
+ this.name,
+ loadData.title,
+ loadData.url,
+ "",
+ Qualities.Unknown.value,
+ isM3u8 = true
+ )
+ )
+ return true
+ }
+}
+
+
+data class Playlist(
+ val items: List = emptyList(),
+)
+
+data class PlaylistItem(
+ val title: String? = null,
+ val attributes: Map = emptyMap(),
+ val headers: Map = emptyMap(),
+ val url: String? = null,
+ val userAgent: String? = null,
+)
+
+
+class IptvPlaylistParser {
+
+
+ /**
+ * Parse M3U8 string into [Playlist]
+ *
+ * @param content M3U8 content string.
+ * @throws PlaylistParserException if an error occurs.
+ */
+ fun parseM3U(content: String): Playlist {
+ return parseM3U(content.byteInputStream())
+ }
+
+ /**
+ * Parse M3U8 content [InputStream] into [Playlist]
+ *
+ * @param input Stream of input data.
+ * @throws PlaylistParserException if an error occurs.
+ */
+ @Throws(PlaylistParserException::class)
+ fun parseM3U(input: InputStream): Playlist {
+ val reader = input.bufferedReader()
+
+ if (!reader.readLine().isExtendedM3u()) {
+ throw PlaylistParserException.InvalidHeader()
+ }
+
+ val playlistItems: MutableList = mutableListOf()
+ var currentIndex = 0
+
+ var line: String? = reader.readLine()
+
+ while (line != null) {
+ if (line.isNotEmpty()) {
+ if (line.startsWith(EXT_INF)) {
+ val title = line.getTitle()
+ val attributes = line.getAttributes()
+ playlistItems.add(PlaylistItem(title, attributes))
+ } else if (line.startsWith(EXT_VLC_OPT)) {
+ val item = playlistItems[currentIndex]
+ val userAgent = line.getTagValue("http-user-agent")
+ val referrer = line.getTagValue("http-referrer")
+ val headers = if (referrer != null) {
+ item.headers + mapOf("referrer" to referrer)
+ } else item.headers
+ playlistItems[currentIndex] =
+ item.copy(userAgent = userAgent, headers = headers)
+ } else {
+ if (!line.startsWith("#")) {
+ val item = playlistItems[currentIndex]
+ val url = line.getUrl()
+ val userAgent = line.getUrlParameter("user-agent")
+ val referrer = line.getUrlParameter("referer")
+ val urlHeaders = if (referrer != null) {
+ item.headers + mapOf("referrer" to referrer)
+ } else item.headers
+ playlistItems[currentIndex] =
+ item.copy(
+ url = url,
+ headers = item.headers + urlHeaders,
+ userAgent = userAgent
+ )
+ currentIndex++
+ }
+ }
+ }
+
+ line = reader.readLine()
+ }
+ return Playlist(playlistItems)
+ }
+
+ /**
+ * Replace "" (quotes) from given string.
+ */
+ private fun String.replaceQuotesAndTrim(): String {
+ return replace("\"", "").trim()
+ }
+
+ /**
+ * Check if given content is valid M3U8 playlist.
+ */
+ private fun String.isExtendedM3u(): Boolean = startsWith(EXT_M3U)
+
+ /**
+ * Get title of media.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * #EXTINF:-1 tvg-id="1234" group-title="Kids" tvg-logo="url/to/logo", Title
+ * ```
+ * Result: Title
+ */
+ private fun String.getTitle(): String? {
+ return split(",").lastOrNull()?.replaceQuotesAndTrim()
+ }
+
+ /**
+ * Get media url.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * https://example.com/sample.m3u8|user-agent="Custom"
+ * ```
+ * Result: https://example.com/sample.m3u8
+ */
+ private fun String.getUrl(): String? {
+ return split("|").firstOrNull()?.replaceQuotesAndTrim()
+ }
+
+ /**
+ * Get url parameters.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * http://192.54.104.122:8080/d/abcdef/video.mp4|User-Agent=Mozilla&Referer=CustomReferrer
+ * ```
+ * Result will be equivalent to kotlin map:
+ * ```Kotlin
+ * mapOf(
+ * "User-Agent" to "Mozilla",
+ * "Referer" to "CustomReferrer"
+ * )
+ * ```
+ */
+ private fun String.getUrlParameters(): Map {
+ val urlRegex = Regex("^(.*)\\|", RegexOption.IGNORE_CASE)
+ val headersString = replace(urlRegex, "").replaceQuotesAndTrim()
+ return headersString.split("&").mapNotNull {
+ val pair = it.split("=")
+ if (pair.size == 2) pair.first() to pair.last() else null
+ }.toMap()
+ }
+
+ /**
+ * Get url parameter with key.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * http://192.54.104.122:8080/d/abcdef/video.mp4|User-Agent=Mozilla&Referer=CustomReferrer
+ * ```
+ * If given key is `user-agent`, then
+ *
+ * Result: Mozilla
+ */
+ private fun String.getUrlParameter(key: String): String? {
+ val urlRegex = Regex("^(.*)\\|", RegexOption.IGNORE_CASE)
+ val keyRegex = Regex("$key=(\\w[^&]*)", RegexOption.IGNORE_CASE)
+ val paramsString = replace(urlRegex, "").replaceQuotesAndTrim()
+ return keyRegex.find(paramsString)?.groups?.get(1)?.value
+ }
+
+ /**
+ * Get attributes from `#EXTINF` tag as Map.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * #EXTINF:-1 tvg-id="1234" group-title="Kids" tvg-logo="url/to/logo", Title
+ * ```
+ * Result will be equivalent to kotlin map:
+ * ```Kotlin
+ * mapOf(
+ * "tvg-id" to "1234",
+ * "group-title" to "Kids",
+ * "tvg-logo" to "url/to/logo"
+ *)
+ * ```
+ */
+ private fun String.getAttributes(): Map {
+ val extInfRegex = Regex("(#EXTINF:.?[0-9]+)", RegexOption.IGNORE_CASE)
+ val attributesString = replace(extInfRegex, "").replaceQuotesAndTrim().split(",").first()
+ return attributesString.split(Regex("\\s")).mapNotNull {
+ val pair = it.split("=")
+ if (pair.size == 2) pair.first() to pair.last()
+ .replaceQuotesAndTrim() else null
+ }.toMap()
+ }
+
+ /**
+ * Get value from a tag.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * #EXTVLCOPT:http-referrer=http://example.com/
+ * ```
+ * Result: http://example.com/
+ */
+ private fun String.getTagValue(key: String): String? {
+ val keyRegex = Regex("$key=(.*)", RegexOption.IGNORE_CASE)
+ return keyRegex.find(this)?.groups?.get(1)?.value?.replaceQuotesAndTrim()
+ }
+
+ companion object {
+ const val EXT_M3U = "#EXTM3U"
+ const val EXT_INF = "#EXTINF"
+ const val EXT_VLC_OPT = "#EXTVLCOPT"
+ }
+
+}
+
+/**
+ * Exception thrown when an error occurs while parsing playlist.
+ */
+sealed class PlaylistParserException(message: String) : Exception(message) {
+
+ /**
+ * Exception thrown if given file content is not valid.
+ */
+ class InvalidHeader :
+ PlaylistParserException("Invalid file header. Header doesn't start with #EXTM3U")
+
+}
\ No newline at end of file
diff --git a/FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProviderPlugin.kt b/FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProviderPlugin.kt
new file mode 100644
index 0000000..826e14e
--- /dev/null
+++ b/FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProviderPlugin.kt
@@ -0,0 +1,14 @@
+
+package com.lagradost
+
+import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
+import com.lagradost.cloudstream3.plugins.Plugin
+import android.content.Context
+
+@CloudstreamPlugin
+class FreeTVProviderPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(FreeTVProvider())
+ }
+}
\ No newline at end of file
diff --git a/IptvorgProvider/build.gradle.kts b/IptvorgProvider/build.gradle.kts
new file mode 100644
index 0000000..9306c22
--- /dev/null
+++ b/IptvorgProvider/build.gradle.kts
@@ -0,0 +1,24 @@
+// use an integer for version numbers
+version = 1
+
+
+cloudstream {
+ // All of these properties are optional, you can safely remove them
+
+ // description = "Lorem Ipsum"
+ authors = listOf("Adippe")
+
+ /**
+ * 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=github.com&sz=%size%"
+}
diff --git a/IptvorgProvider/src/main/AndroidManifest.xml b/IptvorgProvider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29aec9d
--- /dev/null
+++ b/IptvorgProvider/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProvider.kt b/IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProvider.kt
new file mode 100644
index 0000000..4dcbd44
--- /dev/null
+++ b/IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProvider.kt
@@ -0,0 +1,367 @@
+package com.lagradost
+
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import java.io.InputStream
+
+class IptvorgProvider : MainAPI() {
+ override var lang = "en"
+ override var mainUrl = "https://raw.githubusercontent.com/iptv-org/iptv/master/README.md"
+ override var name = "Iptv-org"
+ override val hasMainPage = true
+ override val hasChromecastSupport = true
+ override val supportedTypes = setOf(
+ TvType.Live,
+ )
+
+ override suspend fun getMainPage(
+ page: Int,
+ request : MainPageRequest
+ ): HomePageResponse {
+ val data = app.get(mainUrl).document
+ val table = data.select("tbody")[2].select("td").chunked(3)
+ val shows = table.map { nation ->
+ val channelUrl = nation[2].text()
+ val nationName = nation[0].text()
+ val nationPoster = "https://github.com/emcrisostomo/flags/raw/master/png/256/${channelUrl
+ .substringAfterLast("/")
+ .substringBeforeLast(".").uppercase()}.png"
+ LiveSearchResponse(
+ nationName,
+ LoadData(channelUrl, nationName, nationPoster, 0).toJson(),
+ this.name,
+ TvType.TvSeries,
+ nationPoster,
+ )
+ }
+ return HomePageResponse(
+ listOf(HomePageList(
+ "Nations",
+ shows,
+ true
+ ))
+ )
+
+ }
+
+ override suspend fun search(query: String): List {
+ val data = IptvPlaylistParser().parseM3U(app.get("https://iptv-org.github.io/iptv/index.m3u").text)
+
+ return data.items.filter { it.title?.lowercase()?.contains(query.lowercase()) ?: false }.map { channel ->
+ val streamurl = channel.url.toString()
+ val channelname = channel.attributes["tvg-id"].toString()
+ val posterurl = channel.attributes["tvg-logo"].toString()
+ LiveSearchResponse(
+ channelname,
+ LoadData(streamurl, channelname, posterurl, 1).toJson(),
+ this@IptvorgProvider.name,
+ TvType.Live,
+ posterurl,
+ )
+ }
+ }
+
+
+ override suspend fun load(url: String): LoadResponse {
+ val loadData = parseJson(url)
+
+ if (loadData.flag == 0){
+ val playlist = IptvPlaylistParser().parseM3U(app.get(loadData.url).text)
+ val showlist = playlist.items.mapIndexed { index, channel ->
+ val streamurl = channel.url.toString()
+ val channelname = channel.title.toString()
+ val posterurl = channel.attributes["tvg-logo"].toString()
+ Episode(
+ LoadData(streamurl, channelname, posterurl, 0).toJson(),
+ channelname,
+ null,
+ index + 1,
+ posterurl
+ )
+ }
+
+ return TvSeriesLoadResponse(
+ loadData.channelName,
+ loadData.url,
+ this.name,
+ TvType.TvSeries,
+ showlist,
+ loadData.poster
+ )
+ }
+ else return LiveStreamLoadResponse(
+ loadData.channelName,
+ loadData.url,
+ this.name,
+ LoadData(loadData.url, loadData.channelName, loadData.poster, 0).toJson(),
+ loadData.poster
+ )
+ }
+ data class LoadData(
+ val url: String,
+ val channelName: String,
+ val poster: String,
+ val flag : Int
+ )
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ val loadData = parseJson(data)
+ callback.invoke(
+ ExtractorLink(
+ this@IptvorgProvider.name,
+ loadData.channelName,
+ loadData.url,
+ "",
+ Qualities.Unknown.value,
+ isM3u8 = true
+ )
+ )
+ return true
+ }
+}
+
+
+data class Playlist(
+ val items: List = emptyList(),
+)
+
+data class PlaylistItem(
+ val title: String? = null,
+ val attributes: Map = emptyMap(),
+ val headers: Map = emptyMap(),
+ val url: String? = null,
+ val userAgent: String? = null,
+)
+
+
+class IptvPlaylistParser {
+
+
+ /**
+ * Parse M3U8 string into [Playlist]
+ *
+ * @param content M3U8 content string.
+ * @throws PlaylistParserException if an error occurs.
+ */
+ fun parseM3U(content: String): Playlist {
+ return parseM3U(content.byteInputStream())
+ }
+
+ /**
+ * Parse M3U8 content [InputStream] into [Playlist]
+ *
+ * @param input Stream of input data.
+ * @throws PlaylistParserException if an error occurs.
+ */
+ @Throws(PlaylistParserException::class)
+ fun parseM3U(input: InputStream): Playlist {
+ val reader = input.bufferedReader()
+
+ if (!reader.readLine().isExtendedM3u()) {
+ throw PlaylistParserException.InvalidHeader()
+ }
+
+ val playlistItems: MutableList = mutableListOf()
+ var currentIndex = 0
+
+ var line: String? = reader.readLine()
+
+ while (line != null) {
+ if (line.isNotEmpty()) {
+ if (line.startsWith(EXT_INF)) {
+ val title = line.getTitle()
+ val attributes = line.getAttributes()
+ playlistItems.add(PlaylistItem(title, attributes))
+ } else if (line.startsWith(EXT_VLC_OPT)) {
+ val item = playlistItems[currentIndex]
+ val userAgent = line.getTagValue("http-user-agent")
+ val referrer = line.getTagValue("http-referrer")
+ val headers = if (referrer != null) {
+ item.headers + mapOf("referrer" to referrer)
+ } else item.headers
+ playlistItems[currentIndex] =
+ item.copy(userAgent = userAgent, headers = headers)
+ } else {
+ if (!line.startsWith("#")) {
+ val item = playlistItems[currentIndex]
+ val url = line.getUrl()
+ val userAgent = line.getUrlParameter("user-agent")
+ val referrer = line.getUrlParameter("referer")
+ val urlHeaders = if (referrer != null) {
+ item.headers + mapOf("referrer" to referrer)
+ } else item.headers
+ playlistItems[currentIndex] =
+ item.copy(
+ url = url,
+ headers = item.headers + urlHeaders,
+ userAgent = userAgent
+ )
+ currentIndex++
+ }
+ }
+ }
+
+ line = reader.readLine()
+ }
+ return Playlist(playlistItems)
+ }
+
+ /**
+ * Replace "" (quotes) from given string.
+ */
+ private fun String.replaceQuotesAndTrim(): String {
+ return replace("\"", "").trim()
+ }
+
+ /**
+ * Check if given content is valid M3U8 playlist.
+ */
+ private fun String.isExtendedM3u(): Boolean = startsWith(EXT_M3U)
+
+ /**
+ * Get title of media.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * #EXTINF:-1 tvg-id="1234" group-title="Kids" tvg-logo="url/to/logo", Title
+ * ```
+ * Result: Title
+ */
+ private fun String.getTitle(): String? {
+ return split(",").lastOrNull()?.replaceQuotesAndTrim()
+ }
+
+ /**
+ * Get media url.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * https://example.com/sample.m3u8|user-agent="Custom"
+ * ```
+ * Result: https://example.com/sample.m3u8
+ */
+ private fun String.getUrl(): String? {
+ return split("|").firstOrNull()?.replaceQuotesAndTrim()
+ }
+
+ /**
+ * Get url parameters.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * http://192.54.104.122:8080/d/abcdef/video.mp4|User-Agent=Mozilla&Referer=CustomReferrer
+ * ```
+ * Result will be equivalent to kotlin map:
+ * ```Kotlin
+ * mapOf(
+ * "User-Agent" to "Mozilla",
+ * "Referer" to "CustomReferrer"
+ * )
+ * ```
+ */
+ private fun String.getUrlParameters(): Map {
+ val urlRegex = Regex("^(.*)\\|", RegexOption.IGNORE_CASE)
+ val headersString = replace(urlRegex, "").replaceQuotesAndTrim()
+ return headersString.split("&").mapNotNull {
+ val pair = it.split("=")
+ if (pair.size == 2) pair.first() to pair.last() else null
+ }.toMap()
+ }
+
+ /**
+ * Get url parameter with key.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * http://192.54.104.122:8080/d/abcdef/video.mp4|User-Agent=Mozilla&Referer=CustomReferrer
+ * ```
+ * If given key is `user-agent`, then
+ *
+ * Result: Mozilla
+ */
+ private fun String.getUrlParameter(key: String): String? {
+ val urlRegex = Regex("^(.*)\\|", RegexOption.IGNORE_CASE)
+ val keyRegex = Regex("$key=(\\w[^&]*)", RegexOption.IGNORE_CASE)
+ val paramsString = replace(urlRegex, "").replaceQuotesAndTrim()
+ return keyRegex.find(paramsString)?.groups?.get(1)?.value
+ }
+
+ /**
+ * Get attributes from `#EXTINF` tag as Map.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * #EXTINF:-1 tvg-id="1234" group-title="Kids" tvg-logo="url/to/logo", Title
+ * ```
+ * Result will be equivalent to kotlin map:
+ * ```Kotlin
+ * mapOf(
+ * "tvg-id" to "1234",
+ * "group-title" to "Kids",
+ * "tvg-logo" to "url/to/logo"
+ *)
+ * ```
+ */
+ private fun String.getAttributes(): Map {
+ val extInfRegex = Regex("(#EXTINF:.?[0-9]+)", RegexOption.IGNORE_CASE)
+ val attributesString = replace(extInfRegex, "").replaceQuotesAndTrim().split(",").first()
+ return attributesString.split(Regex("\\s")).mapNotNull {
+ val pair = it.split("=")
+ if (pair.size == 2) pair.first() to pair.last()
+ .replaceQuotesAndTrim() else null
+ }.toMap()
+ }
+
+ /**
+ * Get value from a tag.
+ *
+ * Example:-
+ *
+ * Input:
+ * ```
+ * #EXTVLCOPT:http-referrer=http://example.com/
+ * ```
+ * Result: http://example.com/
+ */
+ private fun String.getTagValue(key: String): String? {
+ val keyRegex = Regex("$key=(.*)", RegexOption.IGNORE_CASE)
+ return keyRegex.find(this)?.groups?.get(1)?.value?.replaceQuotesAndTrim()
+ }
+
+ companion object {
+ const val EXT_M3U = "#EXTM3U"
+ const val EXT_INF = "#EXTINF"
+ const val EXT_VLC_OPT = "#EXTVLCOPT"
+ }
+
+}
+
+/**
+ * Exception thrown when an error occurs while parsing playlist.
+ */
+sealed class PlaylistParserException(message: String) : Exception(message) {
+
+ /**
+ * Exception thrown if given file content is not valid.
+ */
+ class InvalidHeader :
+ PlaylistParserException("Invalid file header. Header doesn't start with #EXTM3U")
+
+}
diff --git a/IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProviderPlugin.kt b/IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProviderPlugin.kt
new file mode 100644
index 0000000..6bdd3bf
--- /dev/null
+++ b/IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProviderPlugin.kt
@@ -0,0 +1,14 @@
+
+package com.lagradost
+
+import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
+import com.lagradost.cloudstream3.plugins.Plugin
+import android.content.Context
+
+@CloudstreamPlugin
+class IptvorgProviderPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(IptvorgProvider())
+ }
+}
\ No newline at end of file