mirror of
https://github.com/iptv-org/iptv
synced 2026-09-16 14:06:34 -04:00
Merge pull request #2087 from iptv-org/add-tvg-country-attribute
Add tvg-country attribute
This commit is contained in:
+8
-2
@@ -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",
|
"code": "us",
|
||||||
"name": "United States"
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
+37
-46
@@ -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.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+200
-195
@@ -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,30 +12,213 @@ 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()
|
||||||
|
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('Done.\n')
|
||||||
|
|
||||||
console.log(
|
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()
|
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
+11
-11
@@ -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}'`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+91
-96
@@ -1,99 +1,117 @@
|
|||||||
const helper = require('./helper')
|
const utils = require('./utils')
|
||||||
|
const parser = require('./parser')
|
||||||
|
const categories = require('./categories')
|
||||||
|
|
||||||
let output = {
|
const list = {
|
||||||
countries: [],
|
countries: {},
|
||||||
languages: [],
|
languages: {},
|
||||||
categories: []
|
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()
|
||||||
|
|
||||||
let countries = {}
|
list.countries['undefined'] = {
|
||||||
let languages = {}
|
country: 'Undefined',
|
||||||
let categories = {}
|
|
||||||
|
|
||||||
for (let categoryCode in helper.supportedCategories) {
|
|
||||||
categories[categoryCode] = {
|
|
||||||
category: helper.supportedCategories[categoryCode],
|
|
||||||
channels: 0,
|
channels: 0,
|
||||||
playlist: `<code>https://iptv-org.github.io/iptv/categories/${categoryCode}.m3u</code>`
|
playlist: `<code>https://iptv-org.github.io/iptv/countries/undefined.m3u</code>`,
|
||||||
}
|
epg: '',
|
||||||
|
name: 'Undefined'
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let rootItem of root.items) {
|
list.languages['undefined'] = {
|
||||||
const playlist = helper.parsePlaylist(rootItem.url)
|
language: 'Undefined',
|
||||||
const countryName = rootItem.name
|
channels: 0,
|
||||||
const countryCode = helper.getBasename(rootItem.url).toLowerCase()
|
playlist: `<code>https://iptv-org.github.io/iptv/languages/undefined.m3u</code>`
|
||||||
const countryEpg = playlist.header.attrs['x-tvg-url']
|
}
|
||||||
? `<code>${playlist.header.attrs['x-tvg-url']}</code>`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
for (let item of playlist.items) {
|
for (const category of categories) {
|
||||||
|
list.categories[category.id] = {
|
||||||
|
category: category.name,
|
||||||
|
channels: 0,
|
||||||
|
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 (const item of items) {
|
||||||
|
const playlist = parser.parsePlaylist(item.url)
|
||||||
|
for (let channel of playlist.channels) {
|
||||||
// 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 + ' ' + countryName,
|
} else {
|
||||||
|
let flag = utils.code2flag(country.code)
|
||||||
|
list.countries[country.code] = {
|
||||||
|
country: flag + ' ' + country.name,
|
||||||
channels: 1,
|
channels: 1,
|
||||||
playlist: `<code>https://iptv-org.github.io/iptv/countries/${countryCode}.m3u</code>`,
|
playlist: `<code>https://iptv-org.github.io/iptv/countries/${country.code}.m3u</code>`,
|
||||||
epg: countryEpg
|
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'
|
|
||||||
if (languageName !== 'Undefined') {
|
|
||||||
languageCode = helper.getISO6391Code(languageName)
|
|
||||||
if (!languageCode) continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (languages[languageCode]) {
|
|
||||||
languages[languageCode].channels++
|
|
||||||
} else {
|
} else {
|
||||||
languages[languageCode] = {
|
for (let language of channel.languages) {
|
||||||
language: languageName,
|
if (list.languages[language.code]) {
|
||||||
|
list.languages[language.code].channels++
|
||||||
|
} else {
|
||||||
|
list.languages[language.code] = {
|
||||||
|
language: language.name,
|
||||||
channels: 1,
|
channels: 1,
|
||||||
playlist: `<code>https://iptv-org.github.io/iptv/languages/${languageCode}.m3u</code>`
|
playlist: `<code>https://iptv-org.github.io/iptv/languages/${language.code}.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
|
||||||
Reference in New Issue
Block a user