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 += `` - } - 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 += `` - i++ - } - output += '\n' - } - output += '\t\n' - - output += '
${column.name}
${item[prop]}
' - - 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 += `` + } + 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 += `` + i++ + } + output += '\n' + } + output += '\t\n' + + output += '
${column.name}
${item[prop]}
' + + 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