diff --git a/.readme/template.md b/.readme/template.md
index 92031dabcb..3211b66679 100644
--- a/.readme/template.md
+++ b/.readme/template.md
@@ -53,16 +53,22 @@ If successful, you should get the following response:
"logo": "https://i.imgur.com/ilZJT5s.png",
"url": "http://ott-cdn.ucom.am/s27/index.m3u8",
"category": "News",
- "language": [
+ "languages": [
{
"code": "eng",
"name": "English"
}
],
- "country": {
- "code": "us",
- "name": "United States"
- },
+ "countries": [
+ {
+ "code": "us",
+ "name": "United States"
+ },
+ {
+ "code": "ca",
+ "name": "Canada"
+ }
+ ],
"tvg": {
"id": "cnn.us",
"name": "CNN",
diff --git a/channels/int.m3u b/channels/int.m3u
deleted file mode 100644
index b3b83de647..0000000000
--- a/channels/int.m3u
+++ /dev/null
@@ -1,145 +0,0 @@
-#EXTM3U
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Anal" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Anal
-http://cdn.adultiptv.net/anal.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Asian" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Asian
-http://cdn.adultiptv.net/asian.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Big Ass" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Big Ass
-http://cdn.adultiptv.net/bigass.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Big Dick" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Big Dick
-http://cdn.adultiptv.net/bigdick.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Big Tits" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Big Tits
-http://cdn.adultiptv.net/bigtits.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Blonde" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Blonde
-http://cdn.adultiptv.net/blonde.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Blowjob" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Blowjob
-http://cdn.adultiptv.net/blowjob.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Brunette" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Brunette
-http://cdn.adultiptv.net/brunette.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Compilation" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Compilation
-http://cdn.adultiptv.net/compilation.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Cuckold" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Cuckold
-http://cdn.adultiptv.net/cuckold.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Fetish" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Fetish
-http://cdn.adultiptv.net/fetish.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Gangbang" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Gangbang
-http://cdn.adultiptv.net/gangbang.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Gay" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Gay
-http://cdn.adultiptv.net/gay.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Hardcore" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Hardcore
-http://cdn.adultiptv.net/hardcore.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Interracial" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Interracial
-http://cdn.adultiptv.net/interracial.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Latina" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Latina
-http://cdn.adultiptv.net/latina.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Lesbian" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Lesbian
-http://cdn.adultiptv.net/lesbian.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Live Cams" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Live Cams
-http://cdn.adultiptv.net/livecams.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net MILF" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net MILF
-http://cdn.adultiptv.net/milf.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Pornstar" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Pornstar
-http://cdn.adultiptv.net/pornstar.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net POV" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net POV
-http://cdn.adultiptv.net/pov.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Rough" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Rough
-http://cdn.adultiptv.net/rough.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Russian" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Russian
-http://cdn.adultiptv.net/russian.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Teen" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Teen
-http://cdn.adultiptv.net/teen.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Threesome" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Threesome
-http://cdn.adultiptv.net/threesome.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/HL7fwzt.png" group-title="Sport",Adventure Sports TV (360p)
-https://gizmeon.s.llnwi.net/channellivev3/live/master.m3u8?channel=275
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/HL7fwzt.png" group-title="",Afrobeats
-https://stream.ecable.tv/afrobeats/tracks-v1a1/mono.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",ANAL
-http://nruxmzi.ojswi5dsmftgm2ldfz4hs6q.cmle.ru/anal.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (HD)
-https://www.ast.tv/stream/1/ultra.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (HQ)
-https://www.ast.tv/stream/1/high.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (LQ)
-https://www.ast.tv/stream/1/normal.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (PC) (1080p)
-http://www.ast.tv/stream/1/master.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (Phone)
-http://www.ast.tv/stream/1/cellular.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (Cellular)
-http://www.ast.tv/stream/2/cellular.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (HD)
-https://www.ast.tv/stream/2/ultra.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (HQ)
-https://www.ast.tv/stream/2/high.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (LQ)
-https://www.ast.tv/stream/2/normal.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (PC) (1080p)
-http://www.ast.tv/stream/2/master.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="Arabic" tvg-logo="https://i.imgur.com/4s1NlRf.jpg" group-title="Religious",Chaine Nord Africaine (360p)
-https://live.creacast.com/cna/smil:cna.smil/chunklist.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/tXTjWgP.png" group-title="Religious",Christian Youth Channel (1080p)
-http://media.smc-host.com:1935/cycnow.com/cyc2/playlist.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",Nyx Media
-https://5a2a51fc4cfde.streamlock.net/free/_definst_/Stream1/chunklist_w805691612.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (1080p)
-https://dms.redbull.tv/v3/linear-borb/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjYXRlZ29yeSI6InBlcnNvbmFsX2NvbXB1dGVyIiwiY291bnRyeV9jb2RlIjoidXMiLCJleHBpcmVzIjoiMjAxNy0wOS0xNlQxNzo0NjowMy45NjM0NjI4NDJaIiwib3NfZmFtaWx5IjoiaHR0cCIsInJlbW90ZV9pcCI6IjEwLjE1Ny4xMTIuMTQ4IiwidWEiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xMl81KSBBcHBsZVdlYktpdC82MDMuMi40IChLSFRNTCwgbGlrZSBHZWNrbykgVmVyc2lvbi8xMC4xLjEgU2FmYXJpLzYwMy4yLjQiLCJ1aWQiOiJkOGZiZWYzMC0yZDhhLTQwYTUtOGNjNy0wNzgxNGJhMTliNzMifQ.Q_38FNpW3so5yrA5FQt9qBuix3dTulKpb6uQ0dRjrtY/playlist.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (1080p)
-https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="Sport",Red Bull TV
-https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_1660.m3u8?xtreamiptv.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="Sport",Red Bull TV
-https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_3360.m3u8?denmstv.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (Canberra / AU) (1080p)
-https://i.mjh.nz/au/Canberra/tv.redbull.tv.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (Melbourne / AU) (1080p)
-https://i.mjh.nz/au/Melbourne/tv.redbull.tv.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (NZ) (1080p)
-https://i.mjh.nz/nz/tv.redbull.tv.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Big Ass
-http://live.redtraffic.xyz/bigass.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Big Dick
-http://live.redtraffic.xyz/bigdick.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Big Tits
-http://live.redtraffic.xyz/bigtits.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Blowjob
-http://live.redtraffic.xyz/blowjob.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Cuckold
-http://live.redtraffic.xyz/cuckold.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Fetish
-http://live.redtraffic.xyz/fetish.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Gangbang
-http://live.redtraffic.net/gangbang.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Hardcore
-http://live.redtraffic.xyz/hardcore.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Interracial
-http://live.redtraffic.xyz/interracial.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Latina
-http://live.redtraffic.xyz/latina.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Lesbian
-http://live.redtraffic.xyz/lesbian.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Milf
-http://live.redtraffic.xyz/milf.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Pornstars
-http://live.redtraffic.xyz/pornstar.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic POV
-http://live.redtraffic.xyz/pov.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Russian
-http://live.redtraffic.xyz/russian.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Teen
-http://live.redtraffic.xyz/teen.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Threesome
-http://live.redtraffic.xyz/threesome.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Woman
-http://live.redtraffic.net/woman.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/826gz7r.jpg" group-title="",Silence TV (720p)
-http://93.190.140.42:8081/SilenceTV/live/playlist.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/ygkBFYT.jpg" group-title="",Swamiji TV (720p)
-https://stream.swamiji.tv/YogaIPTV/smil:YogaStream.smil/playlist.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/5uIXfol.jpg" group-title="",The Boat Show
-https://a.jsrdn.com/broadcast/22706/+0000/hi/c.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/lPyJhBN.png" group-title="News",UN Web TV (540p)
-https://bcliveunivsecure-lh.akamaihd.net/i/un150_A1_1@575439/master.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/Ll6GlqY.png" group-title="Music",V2BEAT TV (720p)
-https://de1se01.v2beat.live/playlist.m3u8
-#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="Persian" tvg-logo="https://i.imgur.com/x6vlBzd.jpg" group-title="",YourTime TV
-https://hls.yourtime.live/hls/stream.m3u8
diff --git a/channels/unsorted.m3u b/channels/unsorted.m3u
index 21d6fb1e9e..07b2d4763e 100644
--- a/channels/unsorted.m3u
+++ b/channels/unsorted.m3u
@@ -365,3 +365,147 @@ https://www.livestreamcdn.net:444/ExtremaTV/_definst_/ExtremaTV/chunklist_w75593
https://y5w8j4a9.ssl.hwcdn.net/mundohd/tracks-v1a1/index.m3u8
#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="" tvg-logo="" group-title="",Дорама
http://sc.id-tv.kz:80/dorama_hd_34_35.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Anal" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Anal
+http://cdn.adultiptv.net/anal.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Asian" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Asian
+http://cdn.adultiptv.net/asian.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Big Ass" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Big Ass
+http://cdn.adultiptv.net/bigass.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Big Dick" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Big Dick
+http://cdn.adultiptv.net/bigdick.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Big Tits" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Big Tits
+http://cdn.adultiptv.net/bigtits.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Blonde" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Blonde
+http://cdn.adultiptv.net/blonde.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Blowjob" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Blowjob
+http://cdn.adultiptv.net/blowjob.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Brunette" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Brunette
+http://cdn.adultiptv.net/brunette.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Compilation" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Compilation
+http://cdn.adultiptv.net/compilation.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Cuckold" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Cuckold
+http://cdn.adultiptv.net/cuckold.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Fetish" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Fetish
+http://cdn.adultiptv.net/fetish.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Gangbang" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Gangbang
+http://cdn.adultiptv.net/gangbang.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Gay" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Gay
+http://cdn.adultiptv.net/gay.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Hardcore" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Hardcore
+http://cdn.adultiptv.net/hardcore.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Interracial" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Interracial
+http://cdn.adultiptv.net/interracial.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Latina" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Latina
+http://cdn.adultiptv.net/latina.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Lesbian" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Lesbian
+http://cdn.adultiptv.net/lesbian.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Live Cams" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Live Cams
+http://cdn.adultiptv.net/livecams.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net MILF" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net MILF
+http://cdn.adultiptv.net/milf.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Pornstar" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Pornstar
+http://cdn.adultiptv.net/pornstar.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net POV" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net POV
+http://cdn.adultiptv.net/pov.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Rough" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Rough
+http://cdn.adultiptv.net/rough.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Russian" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Russian
+http://cdn.adultiptv.net/russian.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Teen" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Teen
+http://cdn.adultiptv.net/teen.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="AdultIPTV.net Threesome" tvg-language="English" tvg-logo="https://files.adultiptv.net/adultiptvnet.jpg" group-title="XXX",AdultIPTV.net Threesome
+http://cdn.adultiptv.net/threesome.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/HL7fwzt.png" group-title="Sport",Adventure Sports TV (360p)
+https://gizmeon.s.llnwi.net/channellivev3/live/master.m3u8?channel=275
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/HL7fwzt.png" group-title="",Afrobeats
+https://stream.ecable.tv/afrobeats/tracks-v1a1/mono.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",ANAL
+http://nruxmzi.ojswi5dsmftgm2ldfz4hs6q.cmle.ru/anal.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (HD)
+https://www.ast.tv/stream/1/ultra.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (HQ)
+https://www.ast.tv/stream/1/high.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (LQ)
+https://www.ast.tv/stream/1/normal.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (PC) (1080p)
+http://www.ast.tv/stream/1/master.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 1 (Phone)
+http://www.ast.tv/stream/1/cellular.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (Cellular)
+http://www.ast.tv/stream/2/cellular.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (HD)
+https://www.ast.tv/stream/2/ultra.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (HQ)
+https://www.ast.tv/stream/2/high.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (LQ)
+https://www.ast.tv/stream/2/normal.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/KmokZz8.jpg" group-title="XXX",AST TV 2 (PC) (1080p)
+http://www.ast.tv/stream/2/master.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="Arabic" tvg-logo="https://i.imgur.com/4s1NlRf.jpg" group-title="Religious",Chaine Nord Africaine (360p)
+https://live.creacast.com/cna/smil:cna.smil/chunklist.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/tXTjWgP.png" group-title="Religious",Christian Youth Channel (1080p)
+http://media.smc-host.com:1935/cycnow.com/cyc2/playlist.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",Nyx Media
+https://5a2a51fc4cfde.streamlock.net/free/_definst_/Stream1/chunklist_w805691612.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (1080p)
+https://dms.redbull.tv/v3/linear-borb/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjYXRlZ29yeSI6InBlcnNvbmFsX2NvbXB1dGVyIiwiY291bnRyeV9jb2RlIjoidXMiLCJleHBpcmVzIjoiMjAxNy0wOS0xNlQxNzo0NjowMy45NjM0NjI4NDJaIiwib3NfZmFtaWx5IjoiaHR0cCIsInJlbW90ZV9pcCI6IjEwLjE1Ny4xMTIuMTQ4IiwidWEiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xMl81KSBBcHBsZVdlYktpdC82MDMuMi40IChLSFRNTCwgbGlrZSBHZWNrbykgVmVyc2lvbi8xMC4xLjEgU2FmYXJpLzYwMy4yLjQiLCJ1aWQiOiJkOGZiZWYzMC0yZDhhLTQwYTUtOGNjNy0wNzgxNGJhMTliNzMifQ.Q_38FNpW3so5yrA5FQt9qBuix3dTulKpb6uQ0dRjrtY/playlist.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (1080p)
+https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="Sport",Red Bull TV
+https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_1660.m3u8?xtreamiptv.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="Sport",Red Bull TV
+https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_3360.m3u8?denmstv.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (Canberra / AU) (1080p)
+https://i.mjh.nz/au/Canberra/tv.redbull.tv.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (Melbourne / AU) (1080p)
+https://i.mjh.nz/au/Melbourne/tv.redbull.tv.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="Red Bull TV" tvg-language="English" tvg-logo="https://i.imgur.com/7NeBmWX.jpg" group-title="Sport",Red Bull TV (NZ) (1080p)
+https://i.mjh.nz/nz/tv.redbull.tv.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Big Ass
+http://live.redtraffic.xyz/bigass.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Big Dick
+http://live.redtraffic.xyz/bigdick.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Big Tits
+http://live.redtraffic.xyz/bigtits.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Blowjob
+http://live.redtraffic.xyz/blowjob.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Cuckold
+http://live.redtraffic.xyz/cuckold.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Fetish
+http://live.redtraffic.xyz/fetish.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Gangbang
+http://live.redtraffic.net/gangbang.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Hardcore
+http://live.redtraffic.xyz/hardcore.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Interracial
+http://live.redtraffic.xyz/interracial.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Latina
+http://live.redtraffic.xyz/latina.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Lesbian
+http://live.redtraffic.xyz/lesbian.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Milf
+http://live.redtraffic.xyz/milf.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Pornstars
+http://live.redtraffic.xyz/pornstar.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic POV
+http://live.redtraffic.xyz/pov.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Russian
+http://live.redtraffic.xyz/russian.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Teen
+http://live.redtraffic.xyz/teen.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Threesome
+http://live.redtraffic.xyz/threesome.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="" group-title="XXX",RedTraffic Woman
+http://live.redtraffic.net/woman.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/826gz7r.jpg" group-title="",Silence TV (720p)
+http://93.190.140.42:8081/SilenceTV/live/playlist.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/ygkBFYT.jpg" group-title="",Swamiji TV (720p)
+https://stream.swamiji.tv/YogaIPTV/smil:YogaStream.smil/playlist.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/5uIXfol.jpg" group-title="",The Boat Show
+https://a.jsrdn.com/broadcast/22706/+0000/hi/c.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/lPyJhBN.png" group-title="News",UN Web TV (540p)
+https://bcliveunivsecure-lh.akamaihd.net/i/un150_A1_1@575439/master.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="English" tvg-logo="https://i.imgur.com/Ll6GlqY.png" group-title="Music",V2BEAT TV (720p)
+https://de1se01.v2beat.live/playlist.m3u8
+#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="Persian" tvg-logo="https://i.imgur.com/x6vlBzd.jpg" group-title="",YourTime TV
+https://hls.yourtime.live/hls/stream.m3u8
diff --git a/index.m3u b/index.m3u
index d727ae7264..f034a2845b 100644
--- a/index.m3u
+++ b/index.m3u
@@ -135,8 +135,6 @@ channels/is.m3u
channels/in.m3u
#EXTINF:-1,Indonesia
channels/id.m3u
-#EXTINF:-1,International
-channels/int.m3u
#EXTINF:-1,Iran
channels/ir.m3u
#EXTINF:-1,Iraq
diff --git a/scripts/categories.json b/scripts/categories.json
new file mode 100644
index 0000000000..8c016cef66
--- /dev/null
+++ b/scripts/categories.json
@@ -0,0 +1,147 @@
+[
+ {
+ "name": "Auto",
+ "id": "auto",
+ "nsfw": false
+ },
+ {
+ "name": "Business",
+ "id": "business",
+ "nsfw": false
+ },
+ {
+ "name": "Classic",
+ "id": "classic",
+ "nsfw": false
+ },
+ {
+ "name": "Comedy",
+ "id": "comedy",
+ "nsfw": false
+ },
+ {
+ "name": "Documentary",
+ "id": "documentary",
+ "nsfw": false
+ },
+ {
+ "name": "Education",
+ "id": "education",
+ "nsfw": false
+ },
+ {
+ "name": "Entertainment",
+ "id": "entertainment",
+ "nsfw": false
+ },
+ {
+ "name": "Family",
+ "id": "family",
+ "nsfw": false
+ },
+ {
+ "name": "Fashion",
+ "id": "fashion",
+ "nsfw": false
+ },
+ {
+ "name": "Food",
+ "id": "food",
+ "nsfw": false
+ },
+ {
+ "name": "General",
+ "id": "general",
+ "nsfw": false
+ },
+ {
+ "name": "Health",
+ "id": "health",
+ "nsfw": false
+ },
+ {
+ "name": "History",
+ "id": "history",
+ "nsfw": false
+ },
+ {
+ "name": "Hobby",
+ "id": "hobby",
+ "nsfw": false
+ },
+ {
+ "name": "Kids",
+ "id": "kids",
+ "nsfw": false
+ },
+ {
+ "name": "Legislative",
+ "id": "legislative",
+ "nsfw": false
+ },
+ {
+ "name": "Lifestyle",
+ "id": "lifestyle",
+ "nsfw": false
+ },
+ {
+ "name": "Local",
+ "id": "local",
+ "nsfw": false
+ },
+ {
+ "name": "Movies",
+ "id": "movies",
+ "nsfw": false
+ },
+ {
+ "name": "Music",
+ "id": "music",
+ "nsfw": false
+ },
+ {
+ "name": "News",
+ "id": "news",
+ "nsfw": false
+ },
+ {
+ "name": "Quiz",
+ "id": "quiz",
+ "nsfw": false
+ },
+ {
+ "name": "Religious",
+ "id": "religious",
+ "nsfw": false
+ },
+ {
+ "name": "Sci-Fi",
+ "id": "sci-fi",
+ "nsfw": false
+ },
+ {
+ "name": "Shop",
+ "id": "shop",
+ "nsfw": false
+ },
+ {
+ "name": "Sport",
+ "id": "sport",
+ "nsfw": false
+ },
+ {
+ "name": "Travel",
+ "id": "travel",
+ "nsfw": false
+ },
+ {
+ "name": "Weather",
+ "id": "weather",
+ "nsfw": false
+ },
+ {
+ "name": "XXX",
+ "id": "xxx",
+ "nsfw": true
+ }
+]
diff --git a/scripts/format.js b/scripts/format.js
index 8edd4682cb..0a69b878c7 100644
--- a/scripts/format.js
+++ b/scripts/format.js
@@ -1,5 +1,6 @@
const { program } = require('commander')
-const helper = require('./helper')
+const parser = require('./parser')
+const utils = require('./utils')
const axios = require('axios')
const ProgressBar = require('progress')
const https = require('https')
@@ -28,23 +29,24 @@ const instance = axios.create({
let globalBuffer = []
async function main() {
- const index = parseIndex()
- for (const item of index.items) {
- await loadPlaylist(item.url)
+ const playlists = parseIndex()
+
+ for (const playlist of playlists) {
+ await loadPlaylist(playlist.url)
.then(addToBuffer)
.then(sortChannels)
.then(removeDuplicates)
.then(detectResolution)
.then(updateFromEPG)
- .then(updatePlaylist)
+ .then(savePlaylist)
.then(done)
}
- if (index.items.length) {
+ if (playlists.length) {
await loadPlaylist('channels/unsorted.m3u')
.then(removeUnsortedDuplicates)
.then(sortChannels)
- .then(updatePlaylist)
+ .then(savePlaylist)
.then(done)
}
@@ -53,38 +55,30 @@ async function main() {
function parseIndex() {
console.info(`Parsing 'index.m3u'...`)
- const playlist = helper.parsePlaylist('index.m3u')
- playlist.items = helper
- .filterPlaylists(playlist.items, config.country, config.exclude)
+ let playlists = parser.parseIndex()
+ playlists = utils
+ .filterPlaylists(playlists, config.country, config.exclude)
.filter(i => i.url !== 'channels/unsorted.m3u')
- console.info(`Found ${playlist.items.length} playlist(s)\n`)
+ console.info(`Found ${playlists.length} playlist(s)\n`)
- return playlist
+ return playlists
}
async function loadPlaylist(url) {
console.info(`Processing '${url}'...`)
- const playlist = helper.parsePlaylist(url)
- playlist.url = url
- playlist.items = playlist.items
- .map(item => {
- return helper.createChannel(item)
- })
- .filter(i => i.url)
-
- return playlist
+ return parser.parsePlaylist(url)
}
async function addToBuffer(playlist) {
if (playlist.url === 'channels/unsorted.m3u') return playlist
- globalBuffer = globalBuffer.concat(playlist.items)
+ globalBuffer = globalBuffer.concat(playlist.channels)
return playlist
}
async function sortChannels(playlist) {
console.info(` Sorting channels...`)
- playlist.items = helper.sortBy(playlist.items, ['name', 'url'])
+ playlist.channels = utils.sortBy(playlist.channels, ['name', 'url'])
return playlist
}
@@ -92,7 +86,7 @@ async function sortChannels(playlist) {
async function removeDuplicates(playlist) {
console.info(` Looking for duplicates...`)
let buffer = {}
- const items = playlist.items.filter(i => {
+ const channels = playlist.channels.filter(i => {
const result = typeof buffer[i.url] === 'undefined'
if (result) {
buffer[i.url] = true
@@ -101,7 +95,7 @@ async function removeDuplicates(playlist) {
return result
})
- playlist.items = items
+ playlist.channels = channels
return playlist
}
@@ -109,31 +103,31 @@ async function removeDuplicates(playlist) {
async function detectResolution(playlist) {
if (!config.resolution) return playlist
const bar = new ProgressBar(' Detecting resolution: [:bar] :current/:total (:percent) ', {
- total: playlist.items.length
+ total: playlist.channels.length
})
const results = []
- for (const item of playlist.items) {
+ for (const channel of playlist.channels) {
bar.tick()
- const url = item.url
+ const url = channel.url
const response = await instance
.get(url)
- .then(helper.sleep(config.delay))
+ .then(utils.sleep(config.delay))
.catch(err => {})
if (response) {
if (response.status === 200) {
if (/^#EXTM3U/.test(response.data)) {
const resolution = parseResolution(response.data)
if (resolution) {
- item.resolution = resolution
+ channel.resolution = resolution
}
}
}
}
- results.push(item)
+ results.push(channel)
}
- playlist.items = results
+ playlist.channels = results
return playlist
}
@@ -160,20 +154,20 @@ async function updateFromEPG(playlist) {
console.info(` Adding data from '${tvgUrl}'...`)
- return helper
+ return utils
.parseEPG(tvgUrl)
.then(epg => {
if (!epg) return playlist
- playlist.items.map(channel => {
+ playlist.channels.map(channel => {
if (!channel.tvg.id) return channel
const epgItem = epg.channels[channel.tvg.id]
if (!epgItem) return channel
if (!channel.tvg.name && epgItem.name.length) {
channel.tvg.name = epgItem.name[0].value
}
- if (!channel.language.length && epgItem.name.length && epgItem.name[0].lang) {
- channel.setLanguage(epgItem.name[0].lang)
+ if (!channel.languages.length && epgItem.name.length && epgItem.name[0].lang) {
+ channel.languages = utils.parseLanguages(epgItem.name[0].lang)
}
if (!channel.logo && epgItem.icon.length) {
channel.logo = epgItem.icon[0]
@@ -190,25 +184,22 @@ async function updateFromEPG(playlist) {
async function removeUnsortedDuplicates(playlist) {
console.info(` Looking for duplicates...`)
const urls = globalBuffer.map(i => i.url)
- const items = playlist.items.filter(i => !urls.includes(i.url))
- if (items.length === playlist.items.length) return playlist
- playlist.items = items
+ const channels = playlist.channels.filter(i => !urls.includes(i.url))
+ if (channels.length === playlist.channels.length) return playlist
+ playlist.channels = channels
return playlist
}
-async function updatePlaylist(playlist) {
- const original = helper.readFile(playlist.url)
- let output = playlist.getHeader()
- for (let channel of playlist.items) {
- output += channel.toShortString()
- }
+async function savePlaylist(playlist) {
+ const original = utils.readFile(playlist.url)
+ const output = playlist.toString(true)
if (original === output) {
console.info(`No changes have been made.`)
return false
} else {
- helper.createFile(playlist.url, output)
+ utils.createFile(playlist.url, output)
console.info(`Playlist has been updated.`)
}
diff --git a/scripts/generate.js b/scripts/generate.js
index 9c42b54b60..7cb05722f9 100644
--- a/scripts/generate.js
+++ b/scripts/generate.js
@@ -1,4 +1,6 @@
-const helper = require('./helper')
+const utils = require('./utils')
+const parser = require('./parser')
+const categories = require('./categories')
const ROOT_DIR = './.gh-pages'
@@ -10,30 +12,213 @@ let list = {
}
function main() {
- console.log(`Parsing index...`)
parseIndex()
- console.log('Creating root directory...')
createRootDirectory()
- console.log('Creating .nojekyll...')
createNoJekyllFile()
- console.log('Generating index.m3u...')
generateIndex()
- console.log('Generating index.sfw.m3u...')
generateSFWIndex()
- console.log('Generating channels.json...')
- generateChannels()
- console.log('Generating index.country.m3u...')
+ generateChannelsJson()
generateCountryIndex()
- console.log('Generating index.language.m3u...')
generateLanguageIndex()
- console.log('Generating index.category.m3u...')
generateCategoryIndex()
- console.log('Generating /countries...')
generateCountries()
- console.log('Generating /categories...')
- generateCategories()
- console.log('Generating /languages...')
generateLanguages()
+ generateCategories()
+ finish()
+}
+
+function createRootDirectory() {
+ console.log('Creating root directory...')
+ utils.createDir(ROOT_DIR)
+}
+
+function createNoJekyllFile() {
+ console.log('Creating .nojekyll...')
+ utils.createFile(`${ROOT_DIR}/.nojekyll`)
+}
+
+function parseIndex() {
+ console.log(`Parsing index...`)
+ const items = parser.parseIndex()
+
+ for (const category of categories) {
+ list.categories[category.id] = []
+ }
+ list.categories['other'] = []
+
+ for (let item of items) {
+ const playlist = parser.parsePlaylist(item.url)
+ for (let channel of playlist.channels) {
+ // all
+ list.all.push(channel)
+
+ // country
+ if (!channel.countries.length) {
+ const countryCode = 'undefined'
+ if (!list.countries[countryCode]) {
+ list.countries[countryCode] = []
+ }
+ list.countries[countryCode].push(channel)
+ } else {
+ for (let country of channel.countries) {
+ const countryCode = country.code || 'undefined'
+ if (!list.countries[countryCode]) {
+ list.countries[countryCode] = []
+ }
+ list.countries[countryCode].push(channel)
+ }
+ }
+
+ // language
+ if (!channel.languages.length) {
+ const languageCode = 'undefined'
+ if (!list.languages[languageCode]) {
+ list.languages[languageCode] = []
+ }
+ list.languages[languageCode].push(channel)
+ } else {
+ for (let language of channel.languages) {
+ const languageCode = language.code || 'undefined'
+ if (!list.languages[languageCode]) {
+ list.languages[languageCode] = []
+ }
+ list.languages[languageCode].push(channel)
+ }
+ }
+
+ // category
+ const categoryId = channel.category.toLowerCase()
+ if (!list.categories[categoryId]) {
+ list.categories['other'].push(channel)
+ } else {
+ list.categories[categoryId].push(channel)
+ }
+ }
+ }
+}
+
+function generateIndex() {
+ console.log('Generating index.m3u...')
+ const filename = `${ROOT_DIR}/index.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ const channels = utils.sortBy(list.all, ['name', 'url'])
+ for (let channel of channels) {
+ utils.appendToFile(filename, channel.toString())
+ }
+}
+
+function generateSFWIndex() {
+ console.log('Generating index.sfw.m3u...')
+ const filename = `${ROOT_DIR}/index.sfw.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ const sorted = utils.sortBy(list.all, ['name', 'url'])
+ const channels = utils.filterNSFW(sorted)
+ for (let channel of channels) {
+ utils.appendToFile(filename, channel.toString())
+ }
+}
+
+function generateChannelsJson() {
+ console.log('Generating channels.json...')
+ const filename = `${ROOT_DIR}/channels.json`
+ const sorted = utils.sortBy(list.all, ['name', 'url'])
+ const channels = sorted.map(c => c.toJSON())
+ utils.createFile(filename, JSON.stringify(channels))
+}
+
+function generateCountryIndex() {
+ console.log('Generating index.country.m3u...')
+ const filename = `${ROOT_DIR}/index.country.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ const channels = utils.sortBy(list.all, ['tvgCountry', 'name', 'url'])
+ for (let channel of channels) {
+ const category = channel.category
+ channel.category = channel.tvgCountry
+ utils.appendToFile(filename, channel.toString())
+ channel.category = category
+ }
+}
+
+function generateLanguageIndex() {
+ console.log('Generating index.language.m3u...')
+ const filename = `${ROOT_DIR}/index.language.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ const channels = utils.sortBy(list.all, ['tvgLanguage', 'name', 'url'])
+ for (let channel of channels) {
+ const category = channel.category
+ channel.category = channel.tvgLanguage
+ utils.appendToFile(filename, channel.toString())
+ channel.category = category
+ }
+}
+
+function generateCategoryIndex() {
+ console.log('Generating index.category.m3u...')
+ const filename = `${ROOT_DIR}/index.category.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ const channels = utils.sortBy(list.all, ['category', 'name', 'url'])
+ for (let channel of channels) {
+ utils.appendToFile(filename, channel.toString())
+ }
+}
+
+function generateCountries() {
+ console.log('Generating /countries...')
+ const outputDir = `${ROOT_DIR}/countries`
+ utils.createDir(outputDir)
+
+ for (const countryId in list.countries) {
+ const filename = `${outputDir}/${countryId}.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ let channels = Object.values(list.countries[countryId])
+ channels = utils.sortBy(channels, ['name', 'url'])
+ for (const channel of channels) {
+ utils.appendToFile(filename, channel.toString())
+ }
+ }
+}
+
+function generateLanguages() {
+ console.log('Generating /languages...')
+ const outputDir = `${ROOT_DIR}/languages`
+ utils.createDir(outputDir)
+
+ for (const languageId in list.languages) {
+ const filename = `${outputDir}/${languageId}.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ let channels = Object.values(list.languages[languageId])
+ channels = utils.sortBy(channels, ['name', 'url'])
+ for (const channel of channels) {
+ utils.appendToFile(filename, channel.toString())
+ }
+ }
+}
+
+function generateCategories() {
+ console.log('Generating /categories...')
+ const outputDir = `${ROOT_DIR}/categories`
+ utils.createDir(outputDir)
+
+ for (const category of categories) {
+ const filename = `${outputDir}/${category.id}.m3u`
+ utils.createFile(filename, '#EXTM3U\n')
+
+ let channels = Object.values(list.categories[category.id])
+ channels = utils.sortBy(channels, ['name', 'url'])
+ for (const channel of channels) {
+ utils.appendToFile(filename, channel.toString())
+ }
+ }
+}
+
+function finish() {
console.log('Done.\n')
console.log(
@@ -43,184 +228,4 @@ function main() {
)
}
-function createRootDirectory() {
- helper.createDir(ROOT_DIR)
-}
-
-function createNoJekyllFile() {
- helper.createFile(`${ROOT_DIR}/.nojekyll`)
-}
-
-function parseIndex() {
- const root = helper.parsePlaylist('index.m3u')
-
- let countries = {}
- let languages = {}
- let categories = {}
-
- for (let rootItem of root.items) {
- const playlist = helper.parsePlaylist(rootItem.url)
- const countryCode = helper.getBasename(rootItem.url).toLowerCase()
- const countryName = rootItem.name
-
- for (let item of playlist.items) {
- const channel = helper.createChannel(item)
- if (!channel.url) continue
- channel.country.code = countryCode
- channel.country.name = countryName
- channel.tvg.url = playlist.header.attrs['x-tvg-url'] || ''
-
- // all
- list.all.push(channel)
-
- // country
- if (!countries[countryCode]) {
- countries[countryCode] = []
- }
- countries[countryCode].push(channel)
-
- // language
- if (!channel.language.length) {
- const languageCode = 'undefined'
- if (!languages[languageCode]) {
- languages[languageCode] = []
- }
- languages[languageCode].push(channel)
- } else {
- for (let language of channel.language) {
- const languageCode = language.code || 'undefined'
- if (!languages[languageCode]) {
- languages[languageCode] = []
- }
- languages[languageCode].push(channel)
- }
- }
-
- // category
- const categoryCode = channel.category ? channel.category.toLowerCase() : 'other'
- if (!categories[categoryCode]) {
- categories[categoryCode] = []
- }
- categories[categoryCode].push(channel)
- }
- }
-
- list.countries = countries
- list.languages = languages
- list.categories = categories
-}
-
-function generateIndex() {
- const filename = `${ROOT_DIR}/index.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const channels = helper.sortBy(list.all, ['name', 'url'])
- for (let channel of channels) {
- helper.appendToFile(filename, channel.toString())
- }
-}
-
-function generateSFWIndex() {
- const filename = `${ROOT_DIR}/index.sfw.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const sorted = helper.sortBy(list.all, ['name', 'url'])
- const channels = helper.filterNSFW(sorted)
- for (let channel of channels) {
- helper.appendToFile(filename, channel.toString())
- }
-}
-
-function generateChannels() {
- const filename = `${ROOT_DIR}/channels.json`
- const sorted = helper.sortBy(list.all, ['name', 'url'])
- const channels = sorted.map(c => c.toJSON())
- helper.createFile(filename, JSON.stringify(channels))
-}
-
-function generateCountryIndex() {
- const filename = `${ROOT_DIR}/index.country.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const channels = helper.sortBy(list.all, ['country.name', 'name', 'url'])
- for (let channel of channels) {
- const category = channel.category
- channel.category = channel.country.name
- helper.appendToFile(filename, channel.toString())
- channel.category = category
- }
-}
-
-function generateLanguageIndex() {
- const filename = `${ROOT_DIR}/index.language.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const channels = helper.sortBy(list.all, ['language.name', 'name', 'url'])
- for (let channel of channels) {
- const category = channel.category
- channel.category = channel.language.map(l => l.name).join(';')
- helper.appendToFile(filename, channel.toString())
- channel.category = category
- }
-}
-
-function generateCategoryIndex() {
- const filename = `${ROOT_DIR}/index.category.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const channels = helper.sortBy(list.all, ['category', 'name', 'url'])
- for (let channel of channels) {
- helper.appendToFile(filename, channel.toString())
- }
-}
-
-function generateCountries() {
- const outputDir = `${ROOT_DIR}/countries`
- helper.createDir(outputDir)
-
- for (let cid in list.countries) {
- let country = list.countries[cid]
- const filename = `${outputDir}/${cid}.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const channels = helper.sortBy(Object.values(country), ['name', 'url'])
- for (let channel of channels) {
- helper.appendToFile(filename, channel.toString())
- }
- }
-}
-
-function generateCategories() {
- const outputDir = `${ROOT_DIR}/categories`
- helper.createDir(outputDir)
-
- for (let cid in helper.supportedCategories) {
- let category = list.categories[cid]
- const filename = `${outputDir}/${cid}.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- if (!category) continue
- const channels = helper.sortBy(Object.values(category), ['name', 'url'])
- for (let channel of channels) {
- helper.appendToFile(filename, channel.toString())
- }
- }
-}
-
-function generateLanguages() {
- const outputDir = `${ROOT_DIR}/languages`
- helper.createDir(outputDir)
-
- for (let lid in list.languages) {
- let language = list.languages[lid]
- const filename = `${outputDir}/${lid}.m3u`
- helper.createFile(filename, '#EXTM3U\n')
-
- const channels = helper.sortBy(Object.values(language), ['name', 'url'])
- for (let channel of channels) {
- helper.appendToFile(filename, channel.toString())
- }
- }
-}
-
main()
diff --git a/scripts/helper.js b/scripts/helper.js
deleted file mode 100644
index fedc1eed92..0000000000
--- a/scripts/helper.js
+++ /dev/null
@@ -1,432 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-const playlistParser = require('iptv-playlist-parser')
-const axios = require('axios')
-const zlib = require('zlib')
-const epgParser = require('epg-parser')
-const urlParser = require('url')
-const escapeStringRegexp = require('escape-string-regexp')
-const markdownInclude = require('markdown-include')
-const iso6393 = require('iso-639-3')
-
-let helper = {}
-
-helper.supportedCategories = {
- auto: 'Auto',
- business: 'Business',
- classic: 'Classic',
- comedy: 'Comedy',
- documentary: 'Documentary',
- education: 'Education',
- entertainment: 'Entertainment',
- family: 'Family',
- fashion: 'Fashion',
- food: 'Food',
- general: 'General',
- health: 'Health',
- history: 'History',
- hobby: 'Hobby',
- kids: 'Kids',
- legislative: 'Legislative',
- lifestyle: 'Lifestyle',
- local: 'Local',
- movies: 'Movies',
- music: 'Music',
- news: 'News',
- quiz: 'Quiz',
- religious: 'Religious',
- 'sci-fi': 'Sci-Fi',
- shop: 'Shop',
- sport: 'Sport',
- travel: 'Travel',
- weather: 'Weather',
- xxx: 'XXX',
- other: 'Other'
-}
-
-helper.code2flag = function (code) {
- switch (code) {
- case 'uk':
- return '🇬🇧'
- case 'int':
- return '🌎'
- case 'unsorted':
- return ''
- default:
- return code
- .toUpperCase()
- .replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397))
- }
-}
-
-helper.sortBy = function (arr, fields) {
- return arr.sort((a, b) => {
- for (let field of fields) {
- let propA = a[field] ? a[field].toLowerCase() : ''
- let propB = b[field] ? b[field].toLowerCase() : ''
-
- if (propA < propB) {
- return -1
- }
- if (propA > propB) {
- return 1
- }
- }
- return 0
- })
-}
-
-helper.createDir = function (dir) {
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir)
- }
-}
-
-helper.compileMarkdown = function (filepath) {
- return markdownInclude.compileFiles(path.resolve(__dirname, filepath))
-}
-
-helper.escapeStringRegexp = function (scring) {
- return escapeStringRegexp(string)
-}
-
-helper.getISO6391Name = function (code) {
- const lang = iso6393.find(l => l.iso6393 === code.toLowerCase())
-
- return lang && lang.name ? lang.name : null
-}
-
-helper.getISO6391Code = function (name) {
- const lang = iso6393.find(l => l.name === name)
-
- return lang && lang.iso6393 ? lang.iso6393 : null
-}
-
-helper.parsePlaylist = function (filename) {
- const content = this.readFile(filename)
- const result = playlistParser.parse(content)
-
- return new Playlist(result)
-}
-
-helper.parseEPG = async function (url) {
- return this.getEPG(url).then(content => {
- const result = epgParser.parse(content)
- console.log('wo')
- const channels = {}
- for (let channel of result.channels) {
- channels[channel.id] = channel
- }
-
- return { url, channels }
- })
-}
-
-helper.getEPG = function (url) {
- return new Promise((resolve, reject) => {
- var buffer = []
- axios({
- method: 'get',
- url: url,
- responseType: 'stream',
- timeout: 60000
- })
- .then(res => {
- let stream
- if (/\.gz$/i.test(url)) {
- let gunzip = zlib.createGunzip()
- res.data.pipe(gunzip)
- stream = gunzip
- } else {
- stream = res.data
- }
-
- stream
- .on('data', function (data) {
- buffer.push(data.toString())
- })
- .on('end', function () {
- resolve(buffer.join(''))
- })
- .on('error', function (e) {
- reject(e)
- })
- })
- .catch(e => {
- reject(e)
- })
- })
-}
-
-helper.readFile = function (filename) {
- return fs.readFileSync(path.resolve(__dirname) + `/../${filename}`, { encoding: 'utf8' })
-}
-
-helper.appendToFile = function (filename, data) {
- fs.appendFileSync(path.resolve(__dirname) + '/../' + filename, data)
-}
-
-helper.createFile = function (filename, data = '') {
- fs.writeFileSync(path.resolve(__dirname) + '/../' + filename, data)
-}
-
-helper.getBasename = function (filename) {
- return path.basename(filename, path.extname(filename))
-}
-
-helper.getUrlPath = function (u) {
- let parsed = urlParser.parse(u)
- let searchQuery = parsed.search || ''
- let path = parsed.host + parsed.pathname + searchQuery
-
- return path.toLowerCase()
-}
-
-helper.generateTable = function (data, options) {
- let output = '
\n'
-
- output += '\t\n\t\t'
- for (let column of options.columns) {
- output += `${column.name} | `
- }
- output += '
\n\t\n'
-
- output += '\t\n'
- for (let item of data) {
- output += '\t\t'
- let i = 0
- for (let prop in item) {
- const column = options.columns[i]
- let nowrap = column.nowrap
- let align = column.align
- output += `${item[prop]} | `
- i++
- }
- output += '
\n'
- }
- output += '\t\n'
-
- output += '
'
-
- return output
-}
-
-helper.createChannel = function (data) {
- return new Channel(data)
-}
-
-helper.writeToLog = function (country, msg, url) {
- var now = new Date()
- var line = `${country}: ${msg} '${url}'`
- this.appendToFile('error.log', now.toISOString() + ' ' + line + '\n')
-}
-
-helper.filterPlaylists = function (arr, include = '', exclude = '') {
- if (include) {
- const included = include.split(',').map(filename => `channels/${filename}.m3u`)
-
- return arr.filter(i => included.indexOf(i.url) > -1)
- }
-
- if (exclude) {
- const excluded = exclude.split(',').map(filename => `channels/${filename}.m3u`)
-
- return arr.filter(i => excluded.indexOf(i.url) === -1)
- }
-
- return arr
-}
-
-helper.filterGroup = function (groupTitle) {
- return this.supportedCategories[groupTitle.toLowerCase()] || ''
-}
-
-helper.filterNSFW = function (arr) {
- const sfwCategories = [
- 'Auto',
- 'Business',
- 'Classic',
- 'Comedy',
- 'Documentary',
- 'Education',
- 'Entertainment',
- 'Family',
- 'Fashion',
- 'Food',
- 'General',
- 'Health',
- 'History',
- 'Hobby',
- 'Kids',
- 'Legislative',
- 'Lifestyle',
- 'Local',
- 'Movies',
- 'Music',
- 'News',
- 'Quiz',
- 'Religious',
- 'Sci-Fi',
- 'Shop',
- 'Sport',
- 'Travel',
- 'Weather'
- ]
-
- return arr.filter(i => sfwCategories.includes(i.category))
-}
-
-helper.sleep = function (ms) {
- return function (x) {
- return new Promise(resolve => setTimeout(() => resolve(x), ms))
- }
-}
-
-class Playlist {
- constructor(data) {
- this.header = data.header
- this.items = data.items
- }
-
- getHeader() {
- let parts = ['#EXTM3U']
- for (let key in this.header.attrs) {
- let value = this.header.attrs[key]
- if (value) {
- parts.push(`${key}="${value}"`)
- }
- }
-
- return `${parts.join(' ')}\n`
- }
-}
-
-class Channel {
- constructor(data) {
- this.parseData(data)
- }
-
- parseData(data) {
- this.logo = data.tvg.logo
- this.category = helper.filterGroup(data.group.title)
- this.url = data.url
- this.name = this.parseName(data.name)
- this.status = this.parseStatus(data.name)
- this.http = data.http
- this.tvg = data.tvg
- this.country = {
- code: null,
- name: null
- }
- this.resolution = this.parseResolution(data.name)
-
- this.setLanguage(data.tvg.language)
- }
-
- get ['language.name']() {
- return this.language[0] ? this.language[0].name : null
- }
-
- get ['country.name']() {
- return this.country.name || null
- }
-
- parseName(title) {
- return title
- .trim()
- .split(' ')
- .map(s => s.trim())
- .filter(s => {
- return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
- })
- .join(' ')
- }
-
- parseStatus(title) {
- const regex = /\[(.*)\]/i
- const match = title.match(regex)
-
- return match ? match[1] : null
- }
-
- parseResolution(title) {
- const regex = /\((\d+)P\)/i
- const match = title.match(regex)
-
- return {
- width: null,
- height: match ? parseInt(match[1]) : null
- }
- }
-
- setLanguage(lang) {
- this.language = lang
- .split(';')
- .map(name => {
- const code = name ? helper.getISO6391Code(name) : null
- if (!code) return null
-
- return {
- code,
- name
- }
- })
- .filter(l => l)
- }
-
- toString() {
- const country = this.country.code ? this.country.code.toUpperCase() : ''
- const tvgUrl = (this.tvg.id || this.tvg.name) && this.tvg.url ? this.tvg.url : ''
- const language = this.language.map(l => l.name).join(';')
- const resolution = this.resolution.height ? ` (${this.resolution.height}p)` : ''
- const status = this.status ? ` [${this.status}]` : ''
-
- let info = `-1 tvg-id="${this.tvg.id}" tvg-name="${this.tvg.name}" tvg-language="${language}" tvg-logo="${this.logo}" tvg-country="${country}" tvg-url="${tvgUrl}" group-title="${this.category}",${this.name}${resolution}${status}`
-
- if (this.http['referrer']) {
- info += `\n#EXTVLCOPT:http-referrer=${this.http['referrer']}`
- }
-
- if (this.http['user-agent']) {
- info += `\n#EXTVLCOPT:http-user-agent=${this.http['user-agent']}`
- }
-
- return '#EXTINF:' + info + '\n' + this.url + '\n'
- }
-
- toShortString() {
- const language = this.language.map(l => l.name).join(';')
- const resolution = this.resolution.height ? ` (${this.resolution.height}p)` : ''
- const status = this.status ? ` [${this.status}]` : ''
-
- let info = `-1 tvg-id="${this.tvg.id}" tvg-name="${this.tvg.name}" tvg-language="${language}" tvg-logo="${this.logo}" group-title="${this.category}",${this.name}${resolution}${status}`
-
- if (this.http['referrer']) {
- info += `\n#EXTVLCOPT:http-referrer=${this.http['referrer']}`
- }
-
- if (this.http['user-agent']) {
- info += `\n#EXTVLCOPT:http-user-agent=${this.http['user-agent']}`
- }
-
- return '#EXTINF:' + info + '\n' + this.url + '\n'
- }
-
- toJSON() {
- return {
- name: this.name,
- logo: this.logo || null,
- url: this.url,
- category: this.category || null,
- language: this.language,
- country: this.country,
- tvg: {
- id: this.tvg.id || null,
- name: this.tvg.name || null,
- url: this.tvg.url || null
- }
- }
- }
-}
-
-module.exports = helper
diff --git a/scripts/parser.js b/scripts/parser.js
new file mode 100644
index 0000000000..15ddc09afb
--- /dev/null
+++ b/scripts/parser.js
@@ -0,0 +1,217 @@
+const playlistParser = require('iptv-playlist-parser')
+const epgParser = require('epg-parser')
+const utils = require('./utils')
+const categories = require('./categories')
+
+const parser = {}
+
+parser.parseIndex = function () {
+ const content = utils.readFile('index.m3u')
+ const result = playlistParser.parse(content)
+
+ return result.items
+}
+
+parser.parsePlaylist = function (filename) {
+ const content = utils.readFile(filename)
+ const result = playlistParser.parse(content)
+
+ return new Playlist({ header: result.header, items: result.items, url: filename })
+}
+
+parser.parseEPG = async function (url) {
+ return utils.loadEPG(url).then(content => {
+ const result = epgParser.parse(content)
+ const channels = {}
+ for (let channel of result.channels) {
+ channels[channel.id] = channel
+ }
+
+ return { url, channels }
+ })
+}
+
+class Playlist {
+ constructor({ header, items, url }) {
+ this.url = url
+ this.header = header
+ this.channels = items
+ .map(item => new Channel({ data: item, header, sourceUrl: url }))
+ .filter(channel => channel.url)
+ }
+
+ toString(short = false) {
+ let parts = ['#EXTM3U']
+ for (let key in this.header.attrs) {
+ let value = this.header.attrs[key]
+ if (value) {
+ parts.push(`${key}="${value}"`)
+ }
+ }
+
+ let output = `${parts.join(' ')}\n`
+ for (let channel of this.channels) {
+ output += channel.toString(short)
+ }
+
+ return output
+ }
+}
+
+class Channel {
+ constructor({ data, header, sourceUrl }) {
+ this.parseData(data)
+
+ if (!this.countries.length) {
+ const filename = utils.getBasename(sourceUrl)
+ const countryName = utils.code2name(filename)
+ this.countries = countryName ? [{ code: filename.toLowerCase(), name: countryName }] : []
+ this.tvg.country = this.countries.map(c => c.code.toUpperCase()).join(';')
+ }
+
+ this.tvg.url = header.attrs['x-tvg-url'] || ''
+ }
+
+ parseData(data) {
+ const title = this.parseTitle(data.name)
+
+ this.tvg = data.tvg
+ this.http = data.http
+ this.url = data.url
+ this.logo = data.tvg.logo
+ this.name = title.channelName
+ this.status = title.streamStatus
+ this.resolution = title.streamResolution
+ this.countries = this.parseCountries(data.tvg.country)
+ this.languages = this.parseLanguages(data.tvg.language)
+ this.category = this.parseCategory(data.group.title)
+ }
+
+ parseCountries(string) {
+ let arr = string
+ .split(';')
+ .reduce((acc, curr) => {
+ const codes = utils.region2codes(curr)
+ if (codes.length) {
+ for (let code of codes) {
+ if (!acc.includes(code)) {
+ acc.push(code)
+ }
+ }
+ } else {
+ acc.push(curr)
+ }
+
+ return acc
+ }, [])
+ .filter(code => code && utils.code2name(code))
+
+ return arr.map(code => {
+ return { code: code.toLowerCase(), name: utils.code2name(code) }
+ })
+ }
+
+ parseLanguages(string) {
+ return string
+ .split(';')
+ .map(name => {
+ const code = name ? utils.language2code(name) : null
+ if (!code) return null
+
+ return {
+ code,
+ name
+ }
+ })
+ .filter(l => l)
+ }
+
+ parseCategory(string) {
+ const category = categories.find(c => c.id === string.toLowerCase())
+
+ return category ? category.name : ''
+ }
+
+ parseTitle(title) {
+ const channelName = title
+ .trim()
+ .split(' ')
+ .map(s => s.trim())
+ .filter(s => {
+ return !/\[|\]/i.test(s) && !/\((\d+)P\)/i.test(s)
+ })
+ .join(' ')
+
+ const streamStatusMatch = title.match(/\[(.*)\]/i)
+ const streamStatus = streamStatusMatch ? streamStatusMatch[1] : null
+
+ const streamResolutionMatch = title.match(/\((\d+)P\)/i)
+ const streamResolutionHeight = streamResolutionMatch ? parseInt(streamResolutionMatch[1]) : null
+ const streamResolution = { width: null, height: streamResolutionHeight }
+
+ return { channelName, streamStatus, streamResolution }
+ }
+
+ get tvgCountry() {
+ return this.tvg.country
+ .split(';')
+ .map(code => utils.code2name(code))
+ .join(';')
+ }
+
+ get tvgLanguage() {
+ return this.tvg.language
+ }
+
+ get tvgUrl() {
+ return (this.tvg.id || this.tvg.name) && this.tvg.url ? this.tvg.url : ''
+ }
+
+ toString(short = false) {
+ this.tvg.country = this.tvg.country.toUpperCase()
+
+ let info = `-1 tvg-id="${this.tvg.id}" tvg-name="${this.tvg.name}" tvg-language="${this.tvg.language}" tvg-logo="${this.logo}" tvg-country="${this.tvg.country}"`
+
+ if (!short) {
+ info += ` tvg-url="${this.tvgUrl}"`
+ }
+
+ info += ` group-title="${this.category}",${this.name}`
+
+ if (this.resolution.height) {
+ info += ` (${this.resolution.height}p)`
+ }
+
+ if (this.status) {
+ info += ` [${this.status}]`
+ }
+
+ if (this.http['referrer']) {
+ info += `\n#EXTVLCOPT:http-referrer=${this.http['referrer']}`
+ }
+
+ if (this.http['user-agent']) {
+ info += `\n#EXTVLCOPT:http-user-agent=${this.http['user-agent']}`
+ }
+
+ return '#EXTINF:' + info + '\n' + this.url + '\n'
+ }
+
+ toJSON() {
+ return {
+ name: this.name,
+ logo: this.logo || null,
+ url: this.url,
+ category: this.category || null,
+ languages: this.languages,
+ countries: this.countries,
+ tvg: {
+ id: this.tvg.id || null,
+ name: this.tvg.name || null,
+ url: this.tvg.url || null
+ }
+ }
+ }
+}
+
+module.exports = parser
diff --git a/scripts/regions.json b/scripts/regions.json
new file mode 100644
index 0000000000..a3626d2c06
--- /dev/null
+++ b/scripts/regions.json
@@ -0,0 +1,1023 @@
+{
+ "AFR": {
+ "name": "Africa",
+ "codes": [
+ "AO",
+ "BF",
+ "BI",
+ "BJ",
+ "BW",
+ "CD",
+ "CF",
+ "CG",
+ "CI",
+ "CM",
+ "CV",
+ "DJ",
+ "DZ",
+ "EG",
+ "EH",
+ "ER",
+ "ET",
+ "GA",
+ "GH",
+ "GM",
+ "GN",
+ "GQ",
+ "GW",
+ "KE",
+ "KM",
+ "LR",
+ "LS",
+ "LY",
+ "MA",
+ "MG",
+ "ML",
+ "MR",
+ "MU",
+ "MW",
+ "MZ",
+ "NA",
+ "NE",
+ "NG",
+ "RE",
+ "RW",
+ "SC",
+ "SD",
+ "SH",
+ "SL",
+ "SN",
+ "SO",
+ "SS",
+ "ST",
+ "SZ",
+ "TD",
+ "TF",
+ "TG",
+ "TN",
+ "TZ",
+ "UG",
+ "YT",
+ "ZA",
+ "ZM",
+ "ZW"
+ ]
+ },
+ "AMER": {
+ "name": "Americas",
+ "codes": [
+ "AG",
+ "AI",
+ "AR",
+ "AW",
+ "BB",
+ "BL",
+ "BM",
+ "BO",
+ "BR",
+ "BS",
+ "BV",
+ "BZ",
+ "CA",
+ "CL",
+ "CO",
+ "CR",
+ "CU",
+ "CW",
+ "DM",
+ "DO",
+ "EC",
+ "FK",
+ "GD",
+ "GF",
+ "GL",
+ "GP",
+ "GS",
+ "GT",
+ "GY",
+ "HN",
+ "HT",
+ "JM",
+ "KN",
+ "KY",
+ "LC",
+ "MF",
+ "MQ",
+ "MS",
+ "MX",
+ "NI",
+ "PA",
+ "PE",
+ "PM",
+ "PR",
+ "PY",
+ "SR",
+ "SV",
+ "SX",
+ "TC",
+ "TT",
+ "US",
+ "UY",
+ "VC",
+ "VE",
+ "VG",
+ "VI"
+ ]
+ },
+ "APAC": {
+ "name": "Asia-Pacific",
+ "codes": [
+ "AE",
+ "AF",
+ "AM",
+ "AS",
+ "AU",
+ "AZ",
+ "BD",
+ "BH",
+ "BN",
+ "BT",
+ "CK",
+ "CN",
+ "CY",
+ "FJ",
+ "FM",
+ "GE",
+ "GU",
+ "ID",
+ "IL",
+ "IN",
+ "IQ",
+ "IR",
+ "JO",
+ "JP",
+ "KG",
+ "KH",
+ "KI",
+ "KP",
+ "KR",
+ "KW",
+ "KZ",
+ "LA",
+ "LB",
+ "LK",
+ "MH",
+ "MM",
+ "MN",
+ "MP",
+ "MV",
+ "MY",
+ "NC",
+ "NF",
+ "NP",
+ "NR",
+ "NU",
+ "NZ",
+ "OM",
+ "PF",
+ "PG",
+ "PH",
+ "PK",
+ "PN",
+ "PS",
+ "PW",
+ "QA",
+ "RU",
+ "SA",
+ "SB",
+ "SG",
+ "SY",
+ "TH",
+ "TJ",
+ "TK",
+ "TL",
+ "TM",
+ "TO",
+ "TR",
+ "TV",
+ "TW",
+ "UZ",
+ "VN",
+ "VU",
+ "WF",
+ "WS",
+ "YE"
+ ]
+ },
+ "ASIA": {
+ "name": "Asia",
+ "codes": [
+ "AE",
+ "AF",
+ "AM",
+ "AZ",
+ "BD",
+ "BH",
+ "BN",
+ "BT",
+ "CN",
+ "CY",
+ "GE",
+ "ID",
+ "IL",
+ "IN",
+ "IQ",
+ "IR",
+ "JO",
+ "JP",
+ "KG",
+ "KH",
+ "KP",
+ "KR",
+ "KW",
+ "KZ",
+ "LA",
+ "LB",
+ "LK",
+ "MM",
+ "MN",
+ "MV",
+ "MY",
+ "NP",
+ "OM",
+ "PH",
+ "PK",
+ "PS",
+ "QA",
+ "RU",
+ "SA",
+ "SG",
+ "SY",
+ "TH",
+ "TJ",
+ "TL",
+ "TM",
+ "TR",
+ "TW",
+ "UZ",
+ "VN",
+ "YE"
+ ]
+ },
+ "CARIB": {
+ "name": "Caribbean",
+ "codes": [
+ "AG",
+ "AI",
+ "AW",
+ "BB",
+ "BL",
+ "BS",
+ "CU",
+ "CW",
+ "DM",
+ "DO",
+ "GD",
+ "GP",
+ "HT",
+ "JM",
+ "KN",
+ "KY",
+ "LC",
+ "MF",
+ "MQ",
+ "MS",
+ "PR",
+ "SX",
+ "TC",
+ "TT",
+ "VC",
+ "VG",
+ "VI"
+ ]
+ },
+ "EMEA": {
+ "name": "Europe, Middle East and Africa",
+ "codes": [
+ "AD",
+ "AE",
+ "AL",
+ "AM",
+ "AO",
+ "AT",
+ "AZ",
+ "BA",
+ "BE",
+ "BF",
+ "BG",
+ "BH",
+ "BI",
+ "BJ",
+ "BW",
+ "BY",
+ "CD",
+ "CF",
+ "CG",
+ "CH",
+ "CI",
+ "CM",
+ "CV",
+ "CY",
+ "CZ",
+ "DE",
+ "DJ",
+ "DK",
+ "DZ",
+ "EE",
+ "EG",
+ "EH",
+ "ER",
+ "ES",
+ "ET",
+ "FI",
+ "FR",
+ "GA",
+ "GB",
+ "GE",
+ "GH",
+ "GM",
+ "GN",
+ "GQ",
+ "GR",
+ "GW",
+ "HR",
+ "HU",
+ "IE",
+ "IQ",
+ "IR",
+ "IS",
+ "IT",
+ "JO",
+ "KE",
+ "KM",
+ "KW",
+ "KZ",
+ "LB",
+ "LI",
+ "LR",
+ "LS",
+ "LT",
+ "LU",
+ "LV",
+ "LY",
+ "MA",
+ "MC",
+ "MD",
+ "ME",
+ "MG",
+ "MK",
+ "ML",
+ "MR",
+ "MT",
+ "MU",
+ "MW",
+ "MZ",
+ "NA",
+ "NE",
+ "NG",
+ "NL",
+ "NO",
+ "OM",
+ "PL",
+ "PS",
+ "PT",
+ "QA",
+ "RE",
+ "RO",
+ "RS",
+ "RU",
+ "RW",
+ "SA",
+ "SC",
+ "SD",
+ "SE",
+ "SH",
+ "SI",
+ "SK",
+ "SL",
+ "SM",
+ "SN",
+ "SO",
+ "SS",
+ "ST",
+ "SY",
+ "SZ",
+ "TD",
+ "TF",
+ "TG",
+ "TN",
+ "TR",
+ "TZ",
+ "UA",
+ "UG",
+ "VA",
+ "YE",
+ "YT",
+ "ZA",
+ "ZM",
+ "ZW"
+ ]
+ },
+ "EUR": {
+ "name": "Europe",
+ "codes": [
+ "AD",
+ "AL",
+ "AM",
+ "AT",
+ "AZ",
+ "BA",
+ "BE",
+ "BG",
+ "BY",
+ "CH",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GB",
+ "GE",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IS",
+ "IT",
+ "KZ",
+ "LI",
+ "LT",
+ "LU",
+ "LV",
+ "MC",
+ "MD",
+ "ME",
+ "MK",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "RS",
+ "RU",
+ "SE",
+ "SI",
+ "SK",
+ "SM",
+ "TR",
+ "UA",
+ "VA"
+ ]
+ },
+ "INT": {
+ "name": "International",
+ "codes": [
+ "AD",
+ "AE",
+ "AF",
+ "AG",
+ "AI",
+ "AL",
+ "AM",
+ "AO",
+ "AQ",
+ "AR",
+ "AS",
+ "AT",
+ "AU",
+ "AW",
+ "AX",
+ "AZ",
+ "BA",
+ "BB",
+ "BD",
+ "BE",
+ "BF",
+ "BG",
+ "BH",
+ "BI",
+ "BJ",
+ "BL",
+ "BM",
+ "BN",
+ "BO",
+ "BQ",
+ "BR",
+ "BS",
+ "BT",
+ "BV",
+ "BW",
+ "BY",
+ "BZ",
+ "CA",
+ "CC",
+ "CD",
+ "CF",
+ "CG",
+ "CH",
+ "CI",
+ "CK",
+ "CL",
+ "CM",
+ "CN",
+ "CO",
+ "CR",
+ "CU",
+ "CV",
+ "CW",
+ "CX",
+ "CY",
+ "CZ",
+ "DE",
+ "DJ",
+ "DK",
+ "DM",
+ "DO",
+ "DZ",
+ "EC",
+ "EE",
+ "EG",
+ "EH",
+ "ER",
+ "ES",
+ "ET",
+ "FI",
+ "FJ",
+ "FK",
+ "FM",
+ "FO",
+ "FR",
+ "GA",
+ "GB",
+ "GD",
+ "GE",
+ "GF",
+ "GG",
+ "GH",
+ "GI",
+ "GL",
+ "GM",
+ "GN",
+ "GP",
+ "GQ",
+ "GR",
+ "GS",
+ "GT",
+ "GU",
+ "GW",
+ "GY",
+ "HK",
+ "HM",
+ "HN",
+ "HR",
+ "HT",
+ "HU",
+ "ID",
+ "IE",
+ "IL",
+ "IM",
+ "IN",
+ "IO",
+ "IQ",
+ "IR",
+ "IS",
+ "IT",
+ "JE",
+ "JM",
+ "JO",
+ "JP",
+ "KE",
+ "KG",
+ "KH",
+ "KI",
+ "KM",
+ "KN",
+ "KP",
+ "KR",
+ "KW",
+ "KY",
+ "KZ",
+ "LA",
+ "LB",
+ "LC",
+ "LI",
+ "LK",
+ "LR",
+ "LS",
+ "LT",
+ "LU",
+ "LV",
+ "LY",
+ "MA",
+ "MC",
+ "MD",
+ "ME",
+ "MF",
+ "MG",
+ "MH",
+ "MK",
+ "ML",
+ "MM",
+ "MN",
+ "MO",
+ "MP",
+ "MQ",
+ "MR",
+ "MS",
+ "MT",
+ "MU",
+ "MV",
+ "MW",
+ "MX",
+ "MY",
+ "MZ",
+ "NA",
+ "NC",
+ "NE",
+ "NF",
+ "NG",
+ "NI",
+ "NL",
+ "NO",
+ "NP",
+ "NR",
+ "NU",
+ "NZ",
+ "OM",
+ "PA",
+ "PE",
+ "PF",
+ "PG",
+ "PH",
+ "PK",
+ "PL",
+ "PM",
+ "PN",
+ "PR",
+ "PS",
+ "PT",
+ "PW",
+ "PY",
+ "QA",
+ "RE",
+ "RO",
+ "RS",
+ "RU",
+ "RW",
+ "SA",
+ "SB",
+ "SC",
+ "SD",
+ "SE",
+ "SG",
+ "SH",
+ "SI",
+ "SJ",
+ "SK",
+ "SL",
+ "SM",
+ "SN",
+ "SO",
+ "SR",
+ "SS",
+ "ST",
+ "SV",
+ "SX",
+ "SY",
+ "SZ",
+ "TC",
+ "TD",
+ "TF",
+ "TG",
+ "TH",
+ "TJ",
+ "TK",
+ "TL",
+ "TM",
+ "TN",
+ "TO",
+ "TR",
+ "TT",
+ "TV",
+ "TW",
+ "TZ",
+ "UA",
+ "UG",
+ "UM",
+ "US",
+ "UY",
+ "UZ",
+ "VA",
+ "VC",
+ "VE",
+ "VG",
+ "VI",
+ "VN",
+ "VU",
+ "WF",
+ "WS",
+ "YE",
+ "YT",
+ "ZA",
+ "ZM",
+ "ZW"
+ ]
+ },
+ "LATAM": {
+ "name": "Latin America",
+ "codes": [
+ "AR",
+ "BL",
+ "BO",
+ "BR",
+ "CL",
+ "CO",
+ "CR",
+ "CU",
+ "DO",
+ "EC",
+ "GF",
+ "GP",
+ "GT",
+ "HN",
+ "HT",
+ "MF",
+ "MQ",
+ "MX",
+ "NI",
+ "PA",
+ "PE",
+ "PR",
+ "PY",
+ "SV",
+ "UY",
+ "VE"
+ ]
+ },
+ "MEA": {
+ "name": "Middle East and Africa",
+ "codes": [
+ "AE",
+ "AO",
+ "BF",
+ "BH",
+ "BI",
+ "BJ",
+ "BW",
+ "CD",
+ "CF",
+ "CG",
+ "CI",
+ "CM",
+ "CV",
+ "DJ",
+ "DZ",
+ "EG",
+ "EH",
+ "ER",
+ "ET",
+ "GA",
+ "GH",
+ "GM",
+ "GN",
+ "GQ",
+ "GW",
+ "IQ",
+ "IR",
+ "JO",
+ "KE",
+ "KM",
+ "KW",
+ "LB",
+ "LR",
+ "LS",
+ "LY",
+ "MA",
+ "MG",
+ "ML",
+ "MR",
+ "MU",
+ "MW",
+ "MZ",
+ "NA",
+ "NE",
+ "NG",
+ "OM",
+ "PS",
+ "QA",
+ "RE",
+ "RW",
+ "SA",
+ "SC",
+ "SD",
+ "SH",
+ "SL",
+ "SN",
+ "SO",
+ "SS",
+ "ST",
+ "SY",
+ "SZ",
+ "TD",
+ "TF",
+ "TG",
+ "TN",
+ "TZ",
+ "UG",
+ "YE",
+ "YT",
+ "ZA",
+ "ZM",
+ "ZW"
+ ]
+ },
+ "MENA": {
+ "name": "Middle East and North Africa",
+ "codes": [
+ "AE",
+ "BH",
+ "DJ",
+ "DZ",
+ "EG",
+ "EH",
+ "IQ",
+ "IR",
+ "JO",
+ "KW",
+ "LB",
+ "LY",
+ "MA",
+ "OM",
+ "PS",
+ "QA",
+ "SA",
+ "SD",
+ "SY",
+ "TN",
+ "YE"
+ ]
+ },
+ "NORD": {
+ "name": "Nordics",
+ "codes": ["AX", "DK", "FO", "FI", "IS", "NO", "SE"]
+ },
+ "OCE": {
+ "name": "Oceania",
+ "codes": [
+ "AS",
+ "AU",
+ "CK",
+ "FJ",
+ "FM",
+ "GU",
+ "KI",
+ "MH",
+ "MP",
+ "NC",
+ "NF",
+ "NR",
+ "NU",
+ "NZ",
+ "PF",
+ "PG",
+ "PN",
+ "PW",
+ "SB",
+ "TK",
+ "TO",
+ "TV",
+ "VU",
+ "WF",
+ "WS"
+ ]
+ },
+ "MIDEAST": {
+ "name": "Middle East",
+ "codes": [
+ "AE",
+ "BH",
+ "CY",
+ "EG",
+ "IL",
+ "IQ",
+ "IR",
+ "JO",
+ "KW",
+ "LB",
+ "OM",
+ "PS",
+ "QA",
+ "SA",
+ "SY",
+ "TR",
+ "YE"
+ ]
+ },
+ "NORAM": {
+ "name": "North America",
+ "codes": [
+ "AG",
+ "AI",
+ "AW",
+ "BB",
+ "BL",
+ "BM",
+ "BS",
+ "BZ",
+ "CA",
+ "CR",
+ "CU",
+ "CW",
+ "DM",
+ "DO",
+ "GD",
+ "GL",
+ "GP",
+ "GT",
+ "HN",
+ "HT",
+ "JM",
+ "KN",
+ "KY",
+ "LC",
+ "MF",
+ "MQ",
+ "MS",
+ "MX",
+ "NI",
+ "PA",
+ "PM",
+ "PR",
+ "SV",
+ "SX",
+ "TC",
+ "TT",
+ "US",
+ "VC",
+ "VG",
+ "VI"
+ ]
+ },
+ "SAS": {
+ "name": "South Asia",
+ "codes": ["AF", "BD", "BT", "IN", "LK", "MV", "NP", "PK"]
+ },
+ "SSA": {
+ "name": "Sub-Saharan Africa",
+ "codes": [
+ "AO",
+ "BF",
+ "BI",
+ "BJ",
+ "BW",
+ "CD",
+ "CF",
+ "CG",
+ "CI",
+ "CM",
+ "CV",
+ "DJ",
+ "ER",
+ "ET",
+ "GA",
+ "GH",
+ "GM",
+ "GN",
+ "GQ",
+ "GW",
+ "KE",
+ "KM",
+ "LR",
+ "LS",
+ "MG",
+ "ML",
+ "MR",
+ "MU",
+ "MW",
+ "MZ",
+ "NA",
+ "NE",
+ "NG",
+ "RW",
+ "SC",
+ "SD",
+ "SL",
+ "SN",
+ "SO",
+ "SS",
+ "ST",
+ "SZ",
+ "TD",
+ "TG",
+ "TZ",
+ "UG",
+ "ZA",
+ "ZM",
+ "ZW"
+ ]
+ }
+}
diff --git a/scripts/test.js b/scripts/test.js
index 1f0ec90870..fca3de471a 100644
--- a/scripts/test.js
+++ b/scripts/test.js
@@ -1,5 +1,6 @@
const { program } = require('commander')
-const helper = require('./helper')
+const utils = require('./utils')
+const parser = require('./parser')
const axios = require('axios')
const https = require('https')
const ProgressBar = require('progress')
@@ -30,28 +31,27 @@ let stats = {
}
async function test() {
- const playlist = helper.parsePlaylist('index.m3u')
+ let items = parser.parseIndex()
+ items = utils.filterPlaylists(items, config.country, config.exclude)
- const countries = helper.filterPlaylists(playlist.items, config.country, config.exclude)
-
- for (let country of countries) {
- const playlist = helper.parsePlaylist(country.url)
- const bar = new ProgressBar(`Processing '${country.url}'...:current/:total\n`, {
- total: playlist.items.length
+ for (const item of items) {
+ const playlist = parser.parsePlaylist(item.url)
+ const bar = new ProgressBar(`Processing '${item.url}'...:current/:total`, {
+ total: playlist.channels.length
})
stats.playlists++
- for (let channel of playlist.items) {
+ for (let channel of playlist.channels) {
bar.tick()
stats.channels++
await instance
.get(channel.url)
- .then(helper.sleep(config.delay))
+ .then(utils.sleep(config.delay))
.catch(error => {
if (error.response) {
stats.failures++
- helper.writeToLog(country.url, error.message, channel.url)
+ utils.writeToLog(country.url, error.message, channel.url)
console.log(`Error: ${error.message} '${channel.url}'`)
}
})
diff --git a/scripts/update-readme.js b/scripts/update-readme.js
index fdea7885d9..bc38c0d933 100644
--- a/scripts/update-readme.js
+++ b/scripts/update-readme.js
@@ -1,99 +1,117 @@
-const helper = require('./helper')
+const utils = require('./utils')
+const parser = require('./parser')
+const categories = require('./categories')
-let output = {
- countries: [],
- languages: [],
- categories: []
+const list = {
+ countries: {},
+ languages: {},
+ categories: {}
}
function main() {
- console.log(`Parsing index...`)
parseIndex()
- console.log(`Generating countries table...`)
generateCountriesTable()
- console.log(`Generating languages table...`)
generateLanguagesTable()
- console.log(`Generating categories table...`)
generateCategoriesTable()
- console.log(`Generating README.md...`)
generateReadme()
- console.log(`Done.`)
+ finish()
}
function parseIndex() {
- const root = helper.parsePlaylist('index.m3u')
+ console.log(`Parsing index...`)
+ const items = parser.parseIndex()
- let countries = {}
- let languages = {}
- let categories = {}
-
- for (let categoryCode in helper.supportedCategories) {
- categories[categoryCode] = {
- category: helper.supportedCategories[categoryCode],
- channels: 0,
- playlist: `https://iptv-org.github.io/iptv/categories/${categoryCode}.m3u
`
- }
+ list.countries['undefined'] = {
+ country: 'Undefined',
+ channels: 0,
+ playlist: `https://iptv-org.github.io/iptv/countries/undefined.m3u
`,
+ epg: '',
+ name: 'Undefined'
}
- for (let rootItem of root.items) {
- const playlist = helper.parsePlaylist(rootItem.url)
- const countryName = rootItem.name
- const countryCode = helper.getBasename(rootItem.url).toLowerCase()
- const countryEpg = playlist.header.attrs['x-tvg-url']
- ? `${playlist.header.attrs['x-tvg-url']}
`
- : ''
+ list.languages['undefined'] = {
+ language: 'Undefined',
+ channels: 0,
+ playlist: `https://iptv-org.github.io/iptv/languages/undefined.m3u
`
+ }
- for (let item of playlist.items) {
+ for (const category of categories) {
+ list.categories[category.id] = {
+ category: category.name,
+ channels: 0,
+ playlist: `https://iptv-org.github.io/iptv/categories/${category.id}.m3u
`
+ }
+ }
+ list.categories['other'] = {
+ category: 'Other',
+ channels: 0,
+ playlist: `https://iptv-org.github.io/iptv/categories/other.m3u
`
+ }
+
+ for (const item of items) {
+ const playlist = parser.parsePlaylist(item.url)
+ for (let channel of playlist.channels) {
// countries
- if (countries[countryCode]) {
- countries[countryCode].channels++
+ if (!channel.countries.length) {
+ list.countries['undefined'].channels++
} else {
- let flag = helper.code2flag(countryCode)
-
- countries[countryCode] = {
- country: flag + ' ' + countryName,
- channels: 1,
- playlist: `https://iptv-org.github.io/iptv/countries/${countryCode}.m3u
`,
- epg: countryEpg
+ for (let country of channel.countries) {
+ if (list.countries[country.code]) {
+ list.countries[country.code].channels++
+ } else {
+ let flag = utils.code2flag(country.code)
+ list.countries[country.code] = {
+ country: flag + ' ' + country.name,
+ channels: 1,
+ playlist: `https://iptv-org.github.io/iptv/countries/${country.code}.m3u
`,
+ epg: playlist.header.attrs['x-tvg-url']
+ ? `${playlist.header.attrs['x-tvg-url']}
`
+ : '',
+ name: country.name
+ }
+ }
}
}
// languages
- const languageNames = item.tvg.language || 'Undefined'
- for (let languageName of languageNames.split(';')) {
- let languageCode = 'undefined'
- if (languageName !== 'Undefined') {
- languageCode = helper.getISO6391Code(languageName)
- if (!languageCode) continue
- }
-
- if (languages[languageCode]) {
- languages[languageCode].channels++
- } else {
- languages[languageCode] = {
- language: languageName,
- channels: 1,
- playlist: `https://iptv-org.github.io/iptv/languages/${languageCode}.m3u
`
+ if (!channel.languages.length) {
+ list.languages['undefined'].channels++
+ } else {
+ for (let language of channel.languages) {
+ if (list.languages[language.code]) {
+ list.languages[language.code].channels++
+ } else {
+ list.languages[language.code] = {
+ language: language.name,
+ channels: 1,
+ playlist: `https://iptv-org.github.io/iptv/languages/${language.code}.m3u
`
+ }
}
}
}
// categories
- const categoryName = helper.filterGroup(item.group.title) || 'Other'
- const categoryCode = categoryName.toLowerCase()
- if (categories[categoryCode]) {
- categories[categoryCode].channels++
+ const categoryId = channel.category.toLowerCase()
+ if (!categoryId) {
+ list.categories['other'].channels++
+ } else if (list.categories[categoryId]) {
+ list.categories[categoryId].channels++
}
}
}
- output.countries = Object.values(countries)
- output.languages = Object.values(languages)
- output.categories = Object.values(categories)
+ list.countries = Object.values(list.countries)
+ list.languages = Object.values(list.languages)
+ list.categories = Object.values(list.categories)
}
function generateCountriesTable() {
- const table = helper.generateTable(output.countries, {
+ console.log(`Generating countries table...`)
+ list.countries = utils.sortBy(list.countries, ['name'])
+ list.countries.forEach(function (i) {
+ delete i.name
+ })
+ const table = utils.generateTable(list.countries, {
columns: [
{ name: 'Country', align: 'left' },
{ name: 'Channels', align: 'right' },
@@ -102,27 +120,13 @@ function generateCountriesTable() {
]
})
- helper.createFile('./.readme/_countries.md', table)
+ utils.createFile('./.readme/_countries.md', table)
}
function generateLanguagesTable() {
- output.languages.sort((a, b) => {
- if (a.language === 'Undefined') {
- return 1
- }
- if (b.language === 'Undefined') {
- return -1
- }
- if (a.language < b.language) {
- return -1
- }
- if (a.language > b.language) {
- return 1
- }
- return 0
- })
-
- const table = helper.generateTable(output.languages, {
+ console.log(`Generating languages table...`)
+ list.languages = utils.sortBy(list.languages, ['language'])
+ const table = utils.generateTable(list.languages, {
columns: [
{ name: 'Language', align: 'left' },
{ name: 'Channels', align: 'right' },
@@ -130,27 +134,13 @@ function generateLanguagesTable() {
]
})
- helper.createFile('./.readme/_languages.md', table)
+ utils.createFile('./.readme/_languages.md', table)
}
function generateCategoriesTable() {
- output.categories.sort((a, b) => {
- if (a.category === 'Other') {
- return 1
- }
- if (b.category === 'Other') {
- return -1
- }
- if (a.category < b.category) {
- return -1
- }
- if (a.category > b.category) {
- return 1
- }
- return 0
- })
-
- const table = helper.generateTable(output.categories, {
+ console.log(`Generating categories table...`)
+ list.categories = utils.sortBy(list.categories, ['category'])
+ const table = utils.generateTable(list.categories, {
columns: [
{ name: 'Category', align: 'left' },
{ name: 'Channels', align: 'right' },
@@ -158,11 +148,16 @@ function generateCategoriesTable() {
]
})
- helper.createFile('./.readme/_categories.md', table)
+ utils.createFile('./.readme/_categories.md', table)
}
function generateReadme() {
- helper.compileMarkdown('../.readme/config.json')
+ console.log(`Generating README.md...`)
+ utils.compileMarkdown('../.readme/config.json')
+}
+
+function finish() {
+ console.log(`Done.`)
}
main()
diff --git a/scripts/utils.js b/scripts/utils.js
new file mode 100644
index 0000000000..f48e79474e
--- /dev/null
+++ b/scripts/utils.js
@@ -0,0 +1,215 @@
+const fs = require('fs')
+const path = require('path')
+const axios = require('axios')
+const zlib = require('zlib')
+const urlParser = require('url')
+const escapeStringRegexp = require('escape-string-regexp')
+const markdownInclude = require('markdown-include')
+const iso6393 = require('iso-639-3')
+const regions = require('./regions')
+const categories = require('./categories')
+const intlDisplayNames = new Intl.DisplayNames(['en'], {
+ style: 'narrow',
+ type: 'region'
+})
+
+const utils = {}
+
+utils.code2flag = function (code) {
+ code = code.toUpperCase()
+ switch (code) {
+ case 'UK':
+ return '🇬🇧'
+ case 'UNSORTED':
+ return ''
+ default:
+ return code.replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397))
+ }
+}
+
+utils.region2codes = function (region) {
+ region = region.toUpperCase()
+
+ return regions[region] ? regions[region].codes : []
+}
+
+utils.code2name = function (code) {
+ try {
+ code = code.toUpperCase()
+ if (regions[code]) return regions[code].name
+ if (code === 'US') return 'United States'
+ return intlDisplayNames.of(code)
+ } catch (e) {
+ return null
+ }
+}
+
+utils.language2code = function (name) {
+ const lang = iso6393.find(l => l.name === name)
+
+ return lang && lang.iso6393 ? lang.iso6393 : null
+}
+
+utils.sortBy = function (arr, fields) {
+ return arr.sort((a, b) => {
+ for (let field of fields) {
+ let propA = a[field] ? a[field].toLowerCase() : ''
+ let propB = b[field] ? b[field].toLowerCase() : ''
+
+ if (propA === 'undefined') {
+ return 1
+ }
+
+ if (propB === 'undefined') {
+ return -1
+ }
+
+ if (propA === 'other') {
+ return 1
+ }
+
+ if (propB === 'other') {
+ return -1
+ }
+
+ if (propA < propB) {
+ return -1
+ }
+ if (propA > propB) {
+ return 1
+ }
+ }
+ return 0
+ })
+}
+
+utils.loadEPG = function (url) {
+ return new Promise((resolve, reject) => {
+ var buffer = []
+ axios({
+ method: 'get',
+ url: url,
+ responseType: 'stream',
+ timeout: 60000
+ })
+ .then(res => {
+ let stream
+ if (/\.gz$/i.test(url)) {
+ let gunzip = zlib.createGunzip()
+ res.data.pipe(gunzip)
+ stream = gunzip
+ } else {
+ stream = res.data
+ }
+
+ stream
+ .on('data', function (data) {
+ buffer.push(data.toString())
+ })
+ .on('end', function () {
+ resolve(buffer.join(''))
+ })
+ .on('error', function (e) {
+ reject(e)
+ })
+ })
+ .catch(e => {
+ reject(e)
+ })
+ })
+}
+
+utils.getBasename = function (filename) {
+ return path.basename(filename, path.extname(filename))
+}
+
+utils.filterPlaylists = function (arr, include = '', exclude = '') {
+ if (include) {
+ const included = include.split(',').map(filename => `channels/${filename}.m3u`)
+
+ return arr.filter(i => included.indexOf(i.url) > -1)
+ }
+
+ if (exclude) {
+ const excluded = exclude.split(',').map(filename => `channels/${filename}.m3u`)
+
+ return arr.filter(i => excluded.indexOf(i.url) === -1)
+ }
+
+ return arr
+}
+
+utils.generateTable = function (data, options) {
+ let output = '\n'
+
+ output += '\t\n\t\t'
+ for (let column of options.columns) {
+ output += `${column.name} | `
+ }
+ output += '
\n\t\n'
+
+ output += '\t\n'
+ for (let item of data) {
+ output += '\t\t'
+ let i = 0
+ for (let prop in item) {
+ const column = options.columns[i]
+ let nowrap = column.nowrap
+ let align = column.align
+ output += `${item[prop]} | `
+ i++
+ }
+ output += '
\n'
+ }
+ output += '\t\n'
+
+ output += '
'
+
+ return output
+}
+
+utils.createDir = function (dir) {
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir)
+ }
+}
+
+utils.readFile = function (filename) {
+ return fs.readFileSync(path.resolve(__dirname) + `/../${filename}`, { encoding: 'utf8' })
+}
+
+utils.appendToFile = function (filename, data) {
+ fs.appendFileSync(path.resolve(__dirname) + '/../' + filename, data)
+}
+
+utils.compileMarkdown = function (filepath) {
+ return markdownInclude.compileFiles(path.resolve(__dirname, filepath))
+}
+
+utils.escapeStringRegexp = function (scring) {
+ return escapeStringRegexp(string)
+}
+
+utils.createFile = function (filename, data = '') {
+ fs.writeFileSync(path.resolve(__dirname) + '/../' + filename, data)
+}
+
+utils.writeToLog = function (country, msg, url) {
+ var now = new Date()
+ var line = `${country}: ${msg} '${url}'`
+ this.appendToFile('error.log', now.toISOString() + ' ' + line + '\n')
+}
+
+utils.filterNSFW = function (arr) {
+ const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
+
+ return arr.filter(i => sfwCategories.includes(i.category))
+}
+
+utils.sleep = function (ms) {
+ return function (x) {
+ return new Promise(resolve => setTimeout(() => resolve(x), ms))
+ }
+}
+
+module.exports = utils