Merge pull request #2087 from iptv-org/add-tvg-country-attribute

Add tvg-country attribute
pull/2115/head
Aleksandr Statciuk 5 years ago committed by GitHub
commit 4534f4c563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -53,16 +53,22 @@ If successful, you should get the following response:
"logo": "https://i.imgur.com/ilZJT5s.png", "logo": "https://i.imgur.com/ilZJT5s.png",
"url": "http://ott-cdn.ucom.am/s27/index.m3u8", "url": "http://ott-cdn.ucom.am/s27/index.m3u8",
"category": "News", "category": "News",
"language": [ "languages": [
{ {
"code": "eng", "code": "eng",
"name": "English" "name": "English"
} }
], ],
"country": { "countries": [
"code": "us", {
"name": "United States" "code": "us",
}, "name": "United States"
},
{
"code": "ca",
"name": "Canada"
}
],
"tvg": { "tvg": {
"id": "cnn.us", "id": "cnn.us",
"name": "CNN", "name": "CNN",

@ -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

@ -365,3 +365,147 @@ https://www.livestreamcdn.net:444/ExtremaTV/_definst_/ExtremaTV/chunklist_w75593
https://y5w8j4a9.ssl.hwcdn.net/mundohd/tracks-v1a1/index.m3u8 https://y5w8j4a9.ssl.hwcdn.net/mundohd/tracks-v1a1/index.m3u8
#EXTINF:-1 tvg-id="" tvg-name="" tvg-language="" tvg-logo="" group-title="",Дорама #EXTINF:-1 tvg-id="" tvg-name="" tvg-language="" tvg-logo="" group-title="",Дорама
http://sc.id-tv.kz:80/dorama_hd_34_35.m3u8 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

@ -135,8 +135,6 @@ channels/is.m3u
channels/in.m3u channels/in.m3u
#EXTINF:-1,Indonesia #EXTINF:-1,Indonesia
channels/id.m3u channels/id.m3u
#EXTINF:-1,International
channels/int.m3u
#EXTINF:-1,Iran #EXTINF:-1,Iran
channels/ir.m3u channels/ir.m3u
#EXTINF:-1,Iraq #EXTINF:-1,Iraq

@ -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
}
]

@ -1,5 +1,6 @@
const { program } = require('commander') const { program } = require('commander')
const helper = require('./helper') const parser = require('./parser')
const utils = require('./utils')
const axios = require('axios') const axios = require('axios')
const ProgressBar = require('progress') const ProgressBar = require('progress')
const https = require('https') const https = require('https')
@ -28,23 +29,24 @@ const instance = axios.create({
let globalBuffer = [] let globalBuffer = []
async function main() { async function main() {
const index = parseIndex() const playlists = parseIndex()
for (const item of index.items) {
await loadPlaylist(item.url) for (const playlist of playlists) {
await loadPlaylist(playlist.url)
.then(addToBuffer) .then(addToBuffer)
.then(sortChannels) .then(sortChannels)
.then(removeDuplicates) .then(removeDuplicates)
.then(detectResolution) .then(detectResolution)
.then(updateFromEPG) .then(updateFromEPG)
.then(updatePlaylist) .then(savePlaylist)
.then(done) .then(done)
} }
if (index.items.length) { if (playlists.length) {
await loadPlaylist('channels/unsorted.m3u') await loadPlaylist('channels/unsorted.m3u')
.then(removeUnsortedDuplicates) .then(removeUnsortedDuplicates)
.then(sortChannels) .then(sortChannels)
.then(updatePlaylist) .then(savePlaylist)
.then(done) .then(done)
} }
@ -53,38 +55,30 @@ async function main() {
function parseIndex() { function parseIndex() {
console.info(`Parsing 'index.m3u'...`) console.info(`Parsing 'index.m3u'...`)
const playlist = helper.parsePlaylist('index.m3u') let playlists = parser.parseIndex()
playlist.items = helper playlists = utils
.filterPlaylists(playlist.items, config.country, config.exclude) .filterPlaylists(playlists, config.country, config.exclude)
.filter(i => i.url !== 'channels/unsorted.m3u') .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) { async function loadPlaylist(url) {
console.info(`Processing '${url}'...`) console.info(`Processing '${url}'...`)
const playlist = helper.parsePlaylist(url) return parser.parsePlaylist(url)
playlist.url = url
playlist.items = playlist.items
.map(item => {
return helper.createChannel(item)
})
.filter(i => i.url)
return playlist
} }
async function addToBuffer(playlist) { async function addToBuffer(playlist) {
if (playlist.url === 'channels/unsorted.m3u') return playlist if (playlist.url === 'channels/unsorted.m3u') return playlist
globalBuffer = globalBuffer.concat(playlist.items) globalBuffer = globalBuffer.concat(playlist.channels)
return playlist return playlist
} }
async function sortChannels(playlist) { async function sortChannels(playlist) {
console.info(` Sorting channels...`) console.info(` Sorting channels...`)
playlist.items = helper.sortBy(playlist.items, ['name', 'url']) playlist.channels = utils.sortBy(playlist.channels, ['name', 'url'])
return playlist return playlist
} }
@ -92,7 +86,7 @@ async function sortChannels(playlist) {
async function removeDuplicates(playlist) { async function removeDuplicates(playlist) {
console.info(` Looking for duplicates...`) console.info(` Looking for duplicates...`)
let buffer = {} let buffer = {}
const items = playlist.items.filter(i => { const channels = playlist.channels.filter(i => {
const result = typeof buffer[i.url] === 'undefined' const result = typeof buffer[i.url] === 'undefined'
if (result) { if (result) {
buffer[i.url] = true buffer[i.url] = true
@ -101,7 +95,7 @@ async function removeDuplicates(playlist) {
return result return result
}) })
playlist.items = items playlist.channels = channels
return playlist return playlist
} }
@ -109,31 +103,31 @@ async function removeDuplicates(playlist) {
async function detectResolution(playlist) { async function detectResolution(playlist) {
if (!config.resolution) return playlist if (!config.resolution) return playlist
const bar = new ProgressBar(' Detecting resolution: [:bar] :current/:total (:percent) ', { const bar = new ProgressBar(' Detecting resolution: [:bar] :current/:total (:percent) ', {
total: playlist.items.length total: playlist.channels.length
}) })
const results = [] const results = []
for (const item of playlist.items) { for (const channel of playlist.channels) {
bar.tick() bar.tick()
const url = item.url const url = channel.url
const response = await instance const response = await instance
.get(url) .get(url)
.then(helper.sleep(config.delay)) .then(utils.sleep(config.delay))
.catch(err => {}) .catch(err => {})
if (response) { if (response) {
if (response.status === 200) { if (response.status === 200) {
if (/^#EXTM3U/.test(response.data)) { if (/^#EXTM3U/.test(response.data)) {
const resolution = parseResolution(response.data) const resolution = parseResolution(response.data)
if (resolution) { if (resolution) {
item.resolution = resolution channel.resolution = resolution
} }
} }
} }
} }
results.push(item) results.push(channel)
} }
playlist.items = results playlist.channels = results
return playlist return playlist
} }
@ -160,20 +154,20 @@ async function updateFromEPG(playlist) {
console.info(` Adding data from '${tvgUrl}'...`) console.info(` Adding data from '${tvgUrl}'...`)
return helper return utils
.parseEPG(tvgUrl) .parseEPG(tvgUrl)
.then(epg => { .then(epg => {
if (!epg) return playlist if (!epg) return playlist
playlist.items.map(channel => { playlist.channels.map(channel => {
if (!channel.tvg.id) return channel if (!channel.tvg.id) return channel
const epgItem = epg.channels[channel.tvg.id] const epgItem = epg.channels[channel.tvg.id]
if (!epgItem) return channel if (!epgItem) return channel
if (!channel.tvg.name && epgItem.name.length) { if (!channel.tvg.name && epgItem.name.length) {
channel.tvg.name = epgItem.name[0].value channel.tvg.name = epgItem.name[0].value
} }
if (!channel.language.length && epgItem.name.length && epgItem.name[0].lang) { if (!channel.languages.length && epgItem.name.length && epgItem.name[0].lang) {
channel.setLanguage(epgItem.name[0].lang) channel.languages = utils.parseLanguages(epgItem.name[0].lang)
} }
if (!channel.logo && epgItem.icon.length) { if (!channel.logo && epgItem.icon.length) {
channel.logo = epgItem.icon[0] channel.logo = epgItem.icon[0]
@ -190,25 +184,22 @@ async function updateFromEPG(playlist) {
async function removeUnsortedDuplicates(playlist) { async function removeUnsortedDuplicates(playlist) {
console.info(` Looking for duplicates...`) console.info(` Looking for duplicates...`)
const urls = globalBuffer.map(i => i.url) const urls = globalBuffer.map(i => i.url)
const items = playlist.items.filter(i => !urls.includes(i.url)) const channels = playlist.channels.filter(i => !urls.includes(i.url))
if (items.length === playlist.items.length) return playlist if (channels.length === playlist.channels.length) return playlist
playlist.items = items playlist.channels = channels
return playlist return playlist
} }
async function updatePlaylist(playlist) { async function savePlaylist(playlist) {
const original = helper.readFile(playlist.url) const original = utils.readFile(playlist.url)
let output = playlist.getHeader() const output = playlist.toString(true)
for (let channel of playlist.items) {
output += channel.toShortString()
}
if (original === output) { if (original === output) {
console.info(`No changes have been made.`) console.info(`No changes have been made.`)
return false return false
} else { } else {
helper.createFile(playlist.url, output) utils.createFile(playlist.url, output)
console.info(`Playlist has been updated.`) console.info(`Playlist has been updated.`)
} }

@ -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' const ROOT_DIR = './.gh-pages'
@ -10,217 +12,220 @@ let list = {
} }
function main() { function main() {
console.log(`Parsing index...`)
parseIndex() parseIndex()
console.log('Creating root directory...')
createRootDirectory() createRootDirectory()
console.log('Creating .nojekyll...')
createNoJekyllFile() createNoJekyllFile()
console.log('Generating index.m3u...')
generateIndex() generateIndex()
console.log('Generating index.sfw.m3u...')
generateSFWIndex() generateSFWIndex()
console.log('Generating channels.json...') generateChannelsJson()
generateChannels()
console.log('Generating index.country.m3u...')
generateCountryIndex() generateCountryIndex()
console.log('Generating index.language.m3u...')
generateLanguageIndex() generateLanguageIndex()
console.log('Generating index.category.m3u...')
generateCategoryIndex() generateCategoryIndex()
console.log('Generating /countries...')
generateCountries() generateCountries()
console.log('Generating /categories...')
generateCategories()
console.log('Generating /languages...')
generateLanguages() generateLanguages()
console.log('Done.\n') generateCategories()
finish()
console.log(
`Countries: ${Object.values(list.countries).length}. Languages: ${
Object.values(list.languages).length
}. Categories: ${Object.values(list.categories).length}. Channels: ${list.all.length}.`
)
} }
function createRootDirectory() { function createRootDirectory() {
helper.createDir(ROOT_DIR) console.log('Creating root directory...')
utils.createDir(ROOT_DIR)
} }
function createNoJekyllFile() { function createNoJekyllFile() {
helper.createFile(`${ROOT_DIR}/.nojekyll`) console.log('Creating .nojekyll...')
utils.createFile(`${ROOT_DIR}/.nojekyll`)
} }
function parseIndex() { function parseIndex() {
const root = helper.parsePlaylist('index.m3u') console.log(`Parsing index...`)
const items = parser.parseIndex()
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) { for (const category of categories) {
const channel = helper.createChannel(item) list.categories[category.id] = []
if (!channel.url) continue }
channel.country.code = countryCode list.categories['other'] = []
channel.country.name = countryName
channel.tvg.url = playlist.header.attrs['x-tvg-url'] || ''
for (let item of items) {
const playlist = parser.parsePlaylist(item.url)
for (let channel of playlist.channels) {
// all // all
list.all.push(channel) list.all.push(channel)
// country // country
if (!countries[countryCode]) { if (!channel.countries.length) {
countries[countryCode] = [] 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)
}
} }
countries[countryCode].push(channel)
// language // language
if (!channel.language.length) { if (!channel.languages.length) {
const languageCode = 'undefined' const languageCode = 'undefined'
if (!languages[languageCode]) { if (!list.languages[languageCode]) {
languages[languageCode] = [] list.languages[languageCode] = []
} }
languages[languageCode].push(channel) list.languages[languageCode].push(channel)
} else { } else {
for (let language of channel.language) { for (let language of channel.languages) {
const languageCode = language.code || 'undefined' const languageCode = language.code || 'undefined'
if (!languages[languageCode]) { if (!list.languages[languageCode]) {
languages[languageCode] = [] list.languages[languageCode] = []
} }
languages[languageCode].push(channel) list.languages[languageCode].push(channel)
} }
} }
// category // category
const categoryCode = channel.category ? channel.category.toLowerCase() : 'other' const categoryId = channel.category.toLowerCase()
if (!categories[categoryCode]) { if (!list.categories[categoryId]) {
categories[categoryCode] = [] list.categories['other'].push(channel)
} else {
list.categories[categoryId].push(channel)
} }
categories[categoryCode].push(channel)
} }
} }
list.countries = countries
list.languages = languages
list.categories = categories
} }
function generateIndex() { function generateIndex() {
console.log('Generating index.m3u...')
const filename = `${ROOT_DIR}/index.m3u` const filename = `${ROOT_DIR}/index.m3u`
helper.createFile(filename, '#EXTM3U\n') utils.createFile(filename, '#EXTM3U\n')
const channels = helper.sortBy(list.all, ['name', 'url']) const channels = utils.sortBy(list.all, ['name', 'url'])
for (let channel of channels) { for (let channel of channels) {
helper.appendToFile(filename, channel.toString()) utils.appendToFile(filename, channel.toString())
} }
} }
function generateSFWIndex() { function generateSFWIndex() {
console.log('Generating index.sfw.m3u...')
const filename = `${ROOT_DIR}/index.sfw.m3u` const filename = `${ROOT_DIR}/index.sfw.m3u`
helper.createFile(filename, '#EXTM3U\n') utils.createFile(filename, '#EXTM3U\n')
const sorted = helper.sortBy(list.all, ['name', 'url']) const sorted = utils.sortBy(list.all, ['name', 'url'])
const channels = helper.filterNSFW(sorted) const channels = utils.filterNSFW(sorted)
for (let channel of channels) { for (let channel of channels) {
helper.appendToFile(filename, channel.toString()) utils.appendToFile(filename, channel.toString())
} }
} }
function generateChannels() { function generateChannelsJson() {
console.log('Generating channels.json...')
const filename = `${ROOT_DIR}/channels.json` const filename = `${ROOT_DIR}/channels.json`
const sorted = helper.sortBy(list.all, ['name', 'url']) const sorted = utils.sortBy(list.all, ['name', 'url'])
const channels = sorted.map(c => c.toJSON()) const channels = sorted.map(c => c.toJSON())
helper.createFile(filename, JSON.stringify(channels)) utils.createFile(filename, JSON.stringify(channels))
} }
function generateCountryIndex() { function generateCountryIndex() {
console.log('Generating index.country.m3u...')
const filename = `${ROOT_DIR}/index.country.m3u` const filename = `${ROOT_DIR}/index.country.m3u`
helper.createFile(filename, '#EXTM3U\n') utils.createFile(filename, '#EXTM3U\n')
const channels = helper.sortBy(list.all, ['country.name', 'name', 'url']) const channels = utils.sortBy(list.all, ['tvgCountry', 'name', 'url'])
for (let channel of channels) { for (let channel of channels) {
const category = channel.category const category = channel.category
channel.category = channel.country.name channel.category = channel.tvgCountry
helper.appendToFile(filename, channel.toString()) utils.appendToFile(filename, channel.toString())
channel.category = category channel.category = category
} }
} }
function generateLanguageIndex() { function generateLanguageIndex() {
console.log('Generating index.language.m3u...')
const filename = `${ROOT_DIR}/index.language.m3u` const filename = `${ROOT_DIR}/index.language.m3u`
helper.createFile(filename, '#EXTM3U\n') utils.createFile(filename, '#EXTM3U\n')
const channels = helper.sortBy(list.all, ['language.name', 'name', 'url']) const channels = utils.sortBy(list.all, ['tvgLanguage', 'name', 'url'])
for (let channel of channels) { for (let channel of channels) {
const category = channel.category const category = channel.category
channel.category = channel.language.map(l => l.name).join(';') channel.category = channel.tvgLanguage
helper.appendToFile(filename, channel.toString()) utils.appendToFile(filename, channel.toString())
channel.category = category channel.category = category
} }
} }
function generateCategoryIndex() { function generateCategoryIndex() {
console.log('Generating index.category.m3u...')
const filename = `${ROOT_DIR}/index.category.m3u` const filename = `${ROOT_DIR}/index.category.m3u`
helper.createFile(filename, '#EXTM3U\n') utils.createFile(filename, '#EXTM3U\n')
const channels = helper.sortBy(list.all, ['category', 'name', 'url']) const channels = utils.sortBy(list.all, ['category', 'name', 'url'])
for (let channel of channels) { for (let channel of channels) {
helper.appendToFile(filename, channel.toString()) utils.appendToFile(filename, channel.toString())
} }
} }
function generateCountries() { function generateCountries() {
console.log('Generating /countries...')
const outputDir = `${ROOT_DIR}/countries` const outputDir = `${ROOT_DIR}/countries`
helper.createDir(outputDir) utils.createDir(outputDir)
for (let cid in list.countries) { for (const countryId in list.countries) {
let country = list.countries[cid] const filename = `${outputDir}/${countryId}.m3u`
const filename = `${outputDir}/${cid}.m3u` utils.createFile(filename, '#EXTM3U\n')
helper.createFile(filename, '#EXTM3U\n')
const channels = helper.sortBy(Object.values(country), ['name', 'url']) let channels = Object.values(list.countries[countryId])
for (let channel of channels) { channels = utils.sortBy(channels, ['name', 'url'])
helper.appendToFile(filename, channel.toString()) for (const channel of channels) {
utils.appendToFile(filename, channel.toString())
} }
} }
} }
function generateCategories() { function generateLanguages() {
const outputDir = `${ROOT_DIR}/categories` console.log('Generating /languages...')
helper.createDir(outputDir) const outputDir = `${ROOT_DIR}/languages`
utils.createDir(outputDir)
for (let cid in helper.supportedCategories) { for (const languageId in list.languages) {
let category = list.categories[cid] const filename = `${outputDir}/${languageId}.m3u`
const filename = `${outputDir}/${cid}.m3u` utils.createFile(filename, '#EXTM3U\n')
helper.createFile(filename, '#EXTM3U\n')
if (!category) continue let channels = Object.values(list.languages[languageId])
const channels = helper.sortBy(Object.values(category), ['name', 'url']) channels = utils.sortBy(channels, ['name', 'url'])
for (let channel of channels) { for (const channel of channels) {
helper.appendToFile(filename, channel.toString()) utils.appendToFile(filename, channel.toString())
} }
} }
} }
function generateLanguages() { function generateCategories() {
const outputDir = `${ROOT_DIR}/languages` console.log('Generating /categories...')
helper.createDir(outputDir) const outputDir = `${ROOT_DIR}/categories`
utils.createDir(outputDir)
for (let lid in list.languages) { for (const category of categories) {
let language = list.languages[lid] const filename = `${outputDir}/${category.id}.m3u`
const filename = `${outputDir}/${lid}.m3u` utils.createFile(filename, '#EXTM3U\n')
helper.createFile(filename, '#EXTM3U\n')
const channels = helper.sortBy(Object.values(language), ['name', 'url']) let channels = Object.values(list.categories[category.id])
for (let channel of channels) { channels = utils.sortBy(channels, ['name', 'url'])
helper.appendToFile(filename, channel.toString()) for (const channel of channels) {
utils.appendToFile(filename, channel.toString())
} }
} }
} }
function finish() {
console.log('Done.\n')
console.log(
`Countries: ${Object.values(list.countries).length}. Languages: ${
Object.values(list.languages).length
}. Categories: ${Object.values(list.categories).length}. Channels: ${list.all.length}.`
)
}
main() main()

@ -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 = '<table>\n'
output += '\t<thead>\n\t\t<tr>'
for (let column of options.columns) {
output += `<th align="${column.align}">${column.name}</th>`
}
output += '</tr>\n\t</thead>\n'
output += '\t<tbody>\n'
for (let item of data) {
output += '\t\t<tr>'
let i = 0
for (let prop in item) {
const column = options.columns[i]
let nowrap = column.nowrap
let align = column.align
output += `<td align="${align}"${nowrap ? ' nowrap' : ''}>${item[prop]}</td>`
i++
}
output += '</tr>\n'
}
output += '\t</tbody>\n'
output += '</table>'
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

@ -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

File diff suppressed because it is too large Load Diff

@ -1,5 +1,6 @@
const { program } = require('commander') const { program } = require('commander')
const helper = require('./helper') const utils = require('./utils')
const parser = require('./parser')
const axios = require('axios') const axios = require('axios')
const https = require('https') const https = require('https')
const ProgressBar = require('progress') const ProgressBar = require('progress')
@ -30,28 +31,27 @@ let stats = {
} }
async function test() { 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 (const item of items) {
const playlist = parser.parsePlaylist(item.url)
for (let country of countries) { const bar = new ProgressBar(`Processing '${item.url}'...:current/:total`, {
const playlist = helper.parsePlaylist(country.url) total: playlist.channels.length
const bar = new ProgressBar(`Processing '${country.url}'...:current/:total\n`, {
total: playlist.items.length
}) })
stats.playlists++ stats.playlists++
for (let channel of playlist.items) { for (let channel of playlist.channels) {
bar.tick() bar.tick()
stats.channels++ stats.channels++
await instance await instance
.get(channel.url) .get(channel.url)
.then(helper.sleep(config.delay)) .then(utils.sleep(config.delay))
.catch(error => { .catch(error => {
if (error.response) { if (error.response) {
stats.failures++ 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}'`) console.log(`Error: ${error.message} '${channel.url}'`)
} }
}) })

@ -1,99 +1,117 @@
const helper = require('./helper') const utils = require('./utils')
const parser = require('./parser')
let output = { const categories = require('./categories')
countries: [],
languages: [], const list = {
categories: [] countries: {},
languages: {},
categories: {}
} }
function main() { function main() {
console.log(`Parsing index...`)
parseIndex() parseIndex()
console.log(`Generating countries table...`)
generateCountriesTable() generateCountriesTable()
console.log(`Generating languages table...`)
generateLanguagesTable() generateLanguagesTable()
console.log(`Generating categories table...`)
generateCategoriesTable() generateCategoriesTable()
console.log(`Generating README.md...`)
generateReadme() generateReadme()
console.log(`Done.`) finish()
} }
function parseIndex() { function parseIndex() {
const root = helper.parsePlaylist('index.m3u') console.log(`Parsing index...`)
const items = parser.parseIndex()
list.countries['undefined'] = {
country: 'Undefined',
channels: 0,
playlist: `<code>https://iptv-org.github.io/iptv/countries/undefined.m3u</code>`,
epg: '',
name: 'Undefined'
}
let countries = {} list.languages['undefined'] = {
let languages = {} language: 'Undefined',
let categories = {} channels: 0,
playlist: `<code>https://iptv-org.github.io/iptv/languages/undefined.m3u</code>`
}
for (let categoryCode in helper.supportedCategories) { for (const category of categories) {
categories[categoryCode] = { list.categories[category.id] = {
category: helper.supportedCategories[categoryCode], category: category.name,
channels: 0, channels: 0,
playlist: `<code>https://iptv-org.github.io/iptv/categories/${categoryCode}.m3u</code>` playlist: `<code>https://iptv-org.github.io/iptv/categories/${category.id}.m3u</code>`
} }
} }
list.categories['other'] = {
category: 'Other',
channels: 0,
playlist: `<code>https://iptv-org.github.io/iptv/categories/other.m3u</code>`
}
for (let rootItem of root.items) { for (const item of items) {
const playlist = helper.parsePlaylist(rootItem.url) const playlist = parser.parsePlaylist(item.url)
const countryName = rootItem.name for (let channel of playlist.channels) {
const countryCode = helper.getBasename(rootItem.url).toLowerCase()
const countryEpg = playlist.header.attrs['x-tvg-url']
? `<code>${playlist.header.attrs['x-tvg-url']}</code>`
: ''
for (let item of playlist.items) {
// countries // countries
if (countries[countryCode]) { if (!channel.countries.length) {
countries[countryCode].channels++ list.countries['undefined'].channels++
} else { } else {
let flag = helper.code2flag(countryCode) for (let country of channel.countries) {
if (list.countries[country.code]) {
countries[countryCode] = { list.countries[country.code].channels++
country: flag + '&nbsp;' + countryName, } else {
channels: 1, let flag = utils.code2flag(country.code)
playlist: `<code>https://iptv-org.github.io/iptv/countries/${countryCode}.m3u</code>`, list.countries[country.code] = {
epg: countryEpg country: flag + '&nbsp;' + country.name,
channels: 1,
playlist: `<code>https://iptv-org.github.io/iptv/countries/${country.code}.m3u</code>`,
epg: playlist.header.attrs['x-tvg-url']
? `<code>${playlist.header.attrs['x-tvg-url']}</code>`
: '',
name: country.name
}
}
} }
} }
// languages // languages
const languageNames = item.tvg.language || 'Undefined' if (!channel.languages.length) {
for (let languageName of languageNames.split(';')) { list.languages['undefined'].channels++
let languageCode = 'undefined' } else {
if (languageName !== 'Undefined') { for (let language of channel.languages) {
languageCode = helper.getISO6391Code(languageName) if (list.languages[language.code]) {
if (!languageCode) continue list.languages[language.code].channels++
} } else {
list.languages[language.code] = {
if (languages[languageCode]) { language: language.name,
languages[languageCode].channels++ channels: 1,
} else { playlist: `<code>https://iptv-org.github.io/iptv/languages/${language.code}.m3u</code>`
languages[languageCode] = { }
language: languageName,
channels: 1,
playlist: `<code>https://iptv-org.github.io/iptv/languages/${languageCode}.m3u</code>`
} }
} }
} }
// categories // categories
const categoryName = helper.filterGroup(item.group.title) || 'Other' const categoryId = channel.category.toLowerCase()
const categoryCode = categoryName.toLowerCase() if (!categoryId) {
if (categories[categoryCode]) { list.categories['other'].channels++
categories[categoryCode].channels++ } else if (list.categories[categoryId]) {
list.categories[categoryId].channels++
} }
} }
} }
output.countries = Object.values(countries) list.countries = Object.values(list.countries)
output.languages = Object.values(languages) list.languages = Object.values(list.languages)
output.categories = Object.values(categories) list.categories = Object.values(list.categories)
} }
function generateCountriesTable() { 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: [ columns: [
{ name: 'Country', align: 'left' }, { name: 'Country', align: 'left' },
{ name: 'Channels', align: 'right' }, { name: 'Channels', align: 'right' },
@ -102,27 +120,13 @@ function generateCountriesTable() {
] ]
}) })
helper.createFile('./.readme/_countries.md', table) utils.createFile('./.readme/_countries.md', table)
} }
function generateLanguagesTable() { function generateLanguagesTable() {
output.languages.sort((a, b) => { console.log(`Generating languages table...`)
if (a.language === 'Undefined') { list.languages = utils.sortBy(list.languages, ['language'])
return 1 const table = utils.generateTable(list.languages, {
}
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, {
columns: [ columns: [
{ name: 'Language', align: 'left' }, { name: 'Language', align: 'left' },
{ name: 'Channels', align: 'right' }, { name: 'Channels', align: 'right' },
@ -130,27 +134,13 @@ function generateLanguagesTable() {
] ]
}) })
helper.createFile('./.readme/_languages.md', table) utils.createFile('./.readme/_languages.md', table)
} }
function generateCategoriesTable() { function generateCategoriesTable() {
output.categories.sort((a, b) => { console.log(`Generating categories table...`)
if (a.category === 'Other') { list.categories = utils.sortBy(list.categories, ['category'])
return 1 const table = utils.generateTable(list.categories, {
}
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, {
columns: [ columns: [
{ name: 'Category', align: 'left' }, { name: 'Category', align: 'left' },
{ name: 'Channels', align: 'right' }, { name: 'Channels', align: 'right' },
@ -158,11 +148,16 @@ function generateCategoriesTable() {
] ]
}) })
helper.createFile('./.readme/_categories.md', table) utils.createFile('./.readme/_categories.md', table)
} }
function generateReadme() { function generateReadme() {
helper.compileMarkdown('../.readme/config.json') console.log(`Generating README.md...`)
utils.compileMarkdown('../.readme/config.json')
}
function finish() {
console.log(`Done.`)
} }
main() main()

@ -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 = '<table>\n'
output += '\t<thead>\n\t\t<tr>'
for (let column of options.columns) {
output += `<th align="${column.align}">${column.name}</th>`
}
output += '</tr>\n\t</thead>\n'
output += '\t<tbody>\n'
for (let item of data) {
output += '\t\t<tr>'
let i = 0
for (let prop in item) {
const column = options.columns[i]
let nowrap = column.nowrap
let align = column.align
output += `<td align="${align}"${nowrap ? ' nowrap' : ''}>${item[prop]}</td>`
i++
}
output += '</tr>\n'
}
output += '\t</tbody>\n'
output += '</table>'
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
Loading…
Cancel
Save