mirror of
https://github.com/recloudstream/cloudstream-extensions-multilingual.git
synced 2024-08-15 03:15:14 +00:00
github.com/Free-TV/IPTV and github.com/iptv-org/iptv providers (#1)
This commit is contained in:
parent
c965f84e47
commit
e3e62c74f6
8 changed files with 790 additions and 0 deletions
24
FreeTVProvider/build.gradle.kts
Normal file
24
FreeTVProvider/build.gradle.kts
Normal file
|
@ -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%"
|
||||
}
|
2
FreeTVProvider/src/main/AndroidManifest.xml
Normal file
2
FreeTVProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
343
FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProvider.kt
Normal file
343
FreeTVProvider/src/main/kotlin/com/lagradost/FreeTVProvider.kt
Normal file
|
@ -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<SearchResponse> {
|
||||
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<LoadData>(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<LoadData>(data)
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
loadData.title,
|
||||
loadData.url,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class Playlist(
|
||||
val items: List<PlaylistItem> = emptyList(),
|
||||
)
|
||||
|
||||
data class PlaylistItem(
|
||||
val title: String? = null,
|
||||
val attributes: Map<String, String> = emptyMap(),
|
||||
val headers: Map<String, String> = 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<PlaylistItem> = 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<String, String> {
|
||||
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<String, String>.
|
||||
*
|
||||
* 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<String, String> {
|
||||
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")
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
24
IptvorgProvider/build.gradle.kts
Normal file
24
IptvorgProvider/build.gradle.kts
Normal file
|
@ -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%"
|
||||
}
|
2
IptvorgProvider/src/main/AndroidManifest.xml
Normal file
2
IptvorgProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.lagradost"/>
|
367
IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProvider.kt
Normal file
367
IptvorgProvider/src/main/kotlin/com/lagradost/IptvorgProvider.kt
Normal file
|
@ -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<SearchResponse> {
|
||||
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<LoadData>(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<LoadData>(data)
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this@IptvorgProvider.name,
|
||||
loadData.channelName,
|
||||
loadData.url,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class Playlist(
|
||||
val items: List<PlaylistItem> = emptyList(),
|
||||
)
|
||||
|
||||
data class PlaylistItem(
|
||||
val title: String? = null,
|
||||
val attributes: Map<String, String> = emptyMap(),
|
||||
val headers: Map<String, String> = 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<PlaylistItem> = 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<String, String> {
|
||||
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<String, String>.
|
||||
*
|
||||
* 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<String, String> {
|
||||
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")
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue