Merge branch 'master' into Sphinxroot-patch-1

pull/3850/head
Shadix A 3 years ago committed by GitHub
commit 736ba5b4f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,103 +4,382 @@ on:
schedule:
- cron: '0 0 * * *'
jobs:
create-branch:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- name: Create Branch
uses: peterjgrainger/action-create-branch@v2.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
branch: 'bot/auto-update'
format:
runs-on: ubuntu-latest
needs: create-branch
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Format Playlists
run: node scripts/format.js
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: '[Bot] Formate playlists'
commit_user_name: iptv-bot
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
branch: bot/auto-update
file_pattern: channels/*
remove-duplicates:
runs-on: ubuntu-latest
needs: format
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Remove Duplicates
run: node scripts/remove-duplicates.js
- name: Upload Artifact
uses: actions/upload-artifact@v2
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
name: channels
path: channels/
filter:
commit_message: '[Bot] Remove duplicates'
commit_user_name: iptv-bot
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
branch: bot/auto-update
file_pattern: channels/*
sort:
runs-on: ubuntu-latest
needs: remove-duplicates
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Sort Channels
run: node scripts/sort.js
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: '[Bot] Sort channels'
commit_user_name: iptv-bot
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
branch: bot/auto-update
file_pattern: channels/*
filter:
runs-on: ubuntu-latest
needs: sort
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Filter Playlists
run: node scripts/filter.js
- name: Upload Artifact
uses: actions/upload-artifact@v2
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
name: channels
path: channels/
format:
commit_message: '[Bot] Filter channels'
commit_user_name: iptv-bot
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
branch: bot/auto-update
file_pattern: channels/*
detect-resolution:
runs-on: ubuntu-latest
needs: filter
continue-on-error: true
strategy:
fail-fast: false
matrix:
country:
[
ad,
ae,
af,
ag,
al,
am,
an,
ao,
ar,
at,
au,
aw,
az,
ba,
bb,
bd,
be,
bf,
bg,
bh,
bn,
bo,
br,
bs,
by,
ca,
cd,
cg,
ch,
ci,
cl,
cm,
cn,
co,
cr,
cu,
cw,
cy,
cz,
de,
dk,
do,
dz,
ec,
ee,
eg,
es,
et,
fi,
fj,
fo,
fr,
ge,
gh,
gm,
gn,
gp,
gq,
gr,
gt,
hk,
hn,
hr,
ht,
hu,
id,
ie,
il,
in,
iq,
ir,
is,
it,
jm,
jo,
jp,
ke,
kg,
kh,
kp,
kr,
kw,
kz,
la,
lb,
li,
lk,
lt,
lu,
lv,
ly,
ma,
mc,
md,
me,
mk,
ml,
mm,
mn,
mo,
mt,
mv,
mx,
my,
mz,
ne,
ng,
ni,
nl,
no,
np,
nz,
om,
pa,
pe,
ph,
pk,
pl,
pr,
ps,
pt,
py,
qa,
ro,
rs,
ru,
rw,
sa,
sd,
se,
sg,
si,
sk,
sl,
sm,
sn,
so,
sv,
sy,
th,
tj,
tm,
tn,
tr,
tt,
tw,
tz,
ua,
ug,
uk,
us,
uy,
uz,
va,
ve,
vi,
vn,
xk,
ye,
zm
]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Format Playlists
run: node scripts/format.js
- name: Detect Resolution
run: node scripts/detect-resolution.js --country=${{ matrix.country }}
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: channels
path: channels/
generate:
path: channels/${{ matrix.country }}.m3u
commit-changes:
runs-on: ubuntu-latest
needs: format
needs: detect-resolution
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: bot/auto-update
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
name: channels
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: '[Bot] Detect resolution'
commit_user_name: iptv-bot
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
branch: bot/auto-update
file_pattern: channels/*
generate:
runs-on: ubuntu-latest
needs: commit-changes
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Generate Playlists
run: node scripts/generate.js
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: gh-pages
path: .gh-pages/
deploy:
if: ${{ github.ref == 'refs/heads/master' }}
runs-on: ubuntu-latest
needs: generate
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: bot/auto-update
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
name: gh-pages
- name: Generate Token
uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
branch: gh-pages
folder: .gh-pages
folder: gh-pages
token: ${{ steps.generate-token.outputs.token }}
git-config-name: iptv-bot
git-config-email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit-message: '[Bot] Deploy to GitHub Pages'
update-readme:
runs-on: ubuntu-latest
needs: generate
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v2
with:
ref: bot/auto-update
- name: Install Dependencies
run: npm install
- name: Update README.md
run: node scripts/update-readme.js
- name: Upload Artifact
uses: actions/upload-artifact@v2
- name: Commit Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
name: README.md
path: README.md
commit_message: '[Bot] Update README.md'
commit_user_name: iptv-bot
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
branch: bot/auto-update
file_pattern: README.md
pull-request:
needs: update-readme
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download /channels
uses: actions/download-artifact@v2
with:
name: channels
path: channels/
- name: Download README.md
uses: actions/download-artifact@v2
with:
name: README.md
ref: bot/auto-update
- name: Generate Token
uses: tibdex/github-app-token@v1
id: generate-token
@ -109,26 +388,25 @@ jobs:
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Create Pull Request
id: pr
uses: peter-evans/create-pull-request@v3
uses: repo-sync/pull-request@v2
with:
title: '[Bot] Update playlists'
body: |
This pull request is created automatically by `auto-update` action.
commit-message: '[Bot] Update playlists'
committer: GitHub <noreply@github.com>
branch: bot/auto-update
delete-branch: true
token: ${{ steps.generate-token.outputs.token }}
source_branch: 'bot/auto-update'
destination_branch: 'master'
pr_title: '[Bot] Update playlists'
pr_body: |
This pull request is created by [auto-update][1] workflow.
[1]: https://github.com/iptv-org/iptv/actions/runs/${{ github.run_id }}
github_token: ${{ steps.generate-token.outputs.token }}
- name: Enable Pull Request Automerge
if: steps.pr.outputs.pull-request-operation == 'created'
uses: peter-evans/enable-pull-request-automerge@v1
with:
token: ${{ secrets.PAT }}
pull-request-number: ${{ steps.pr.outputs.pull-request-number }}
pull-request-number: ${{ steps.pr.outputs.pr_number }}
merge-method: squash
- name: Approve Pull Request
if: steps.pr.outputs.pull-request-operation == 'created'
if: github.ref == 'refs/heads/master'
uses: juliangruber/approve-pull-request-action@v1
with:
github-token: ${{ secrets.PAT }}
number: ${{ steps.pr.outputs.pull-request-number }}
number: ${{ steps.pr.outputs.pr_number }}

@ -167,18 +167,15 @@ http://example.com/stream.m3u8
- ...
- `unsorted.m3u`: playlist with channels not yet sorted.
- `scripts/`
- `blacklist.json`: list of channels banned for addition to the repository.
- `categories.json`: list of supported categories.
- `helpers/`: helper scripts used in GitHub Actions.
- `clean.js`: used in GitHub Action to check all links and remove broken ones.
- `db.js`: contains functions for retrieving and managing the channel list.
- `detect-resolution.js`: used in GitHub Action to detect resolution of the streams.
- `filter.js`: used within GitHub Action to remove blacklisted channels from playlists.
- `format.js`: used within GitHub Action to format channel descriptions and sort playlists.
- `format.js`: used within GitHub Action to format channel descriptions.
- `generate.js`: used within GitHub Action to generate all additional playlists.
- `parser.js`: contains functions for parsing playlists.
- `regions.json`: list of supported region codes.
- `remove-duplicates.js`: used in GitHub Action to remove duplicates from the playlist.
- `sort.js`: used within GitHub Action to sort channels by name.
- `update-readme.js`: used within GitHub Action to update the `README.md` file.
- `utils.js`: contains functions that are used in other scripts.
- `CONTRIBUTING.md`: file you are currently reading.
- `index.m3u`: main playlist that contains links to all playlists in the `channels/` folder.
- `README.md`: project description generated from the contents of the `.readme/` folder.

@ -5,6 +5,8 @@ http://45.162.230.234:1935/agrobrasiltv/agrobrasiltv/playlist.m3u8
https://5cf4a2c2512a2.streamlock.net/dgrau/dgrau/playlist.m3u8
#EXTINF:-1 tvg-id="Animestation.br" tvg-name="Animestation" tvg-country="BR" tvg-language="" tvg-logo="https://i.imgur.com/5UpjGcL.png" group-title="",Animestation (480p) [Not 24/7]
https://stmv.video.expressolider.com.br/animestation1/animestation1/playlist.m3u8
#EXTINF:-1 tvg-id="AnimeTV.br" tvg-name="AnimeTV" tvg-country="BR" tvg-language="Japanese" tvg-logo="" group-title="Animation",Anime TV
https://stmv1.srvif.com/animetv/animetv/playlist.m3u8
#EXTINF:-1 tvg-id="Bandnews.br" tvg-name="Bandnews" tvg-country="BR" tvg-language="Portuguese" tvg-logo="https://i.imgur.com/RehSh6T.png" group-title="News",Bandnews
http://212.224.98.205:2200/BR/Band_News_HD-br/index.m3u8?token=
#EXTINF:-1 tvg-id="CGhost.br" tvg-name="C-Ghost" tvg-country="BR" tvg-language="" tvg-logo="https://i.imgur.com/ZVO8GVI.png" group-title="",C-Ghost (1080p)

@ -153,7 +153,7 @@ https://livedoc.cgtn.com/500d/prog_index.m3u8
https://news.cgtn.com/resource/live/document/cgtn-doc.m3u8
#EXTINF:-1 tvg-id="CGTNDocumentaryZhongGuo.cn" tvg-name="CGTN Documentary(中国)" tvg-country="CN" tvg-language="Chinese" tvg-logo="http://static.epg.best/au/CGTNDocu.au.png" group-title="News",CGTN Documentary(中国)
https://livedoc.cgtn.com/1000d/prog_index.m3u8
#EXTINF:-1 tvg-id="CGTNEspanol.cn" tvg-name="CGTN Español" tvg-country="CN" tvg-language="" tvg-logo="" group-title="",CGTN Español
#EXTINF:-1 tvg-id="CGTNEspanol.cn" tvg-name="CGTN Español" tvg-country="CN;LATAM;ES" tvg-language="Spanish" tvg-logo="https://i.imgur.com/yXc4j9J.png" group-title="",CGTN Español
http://livees.cgtn.com/500e/prog_index.m3u8
#EXTINF:-1 tvg-id="CGTNFrance.cn" tvg-name="CGTN France" tvg-country="FR" tvg-language="French" tvg-logo="https://i.imgur.com/yXc4j9J.png" group-title="",CGTN France
http://livefr.cgtn.com/1000f/prog_index.m3u8

@ -61,6 +61,8 @@ http://103.199.161.254/Content/amrita/Live/Channel(Amrita)/index.m3u8
https://f3.vstream.online:7443/bstb/ngrp:anjan_hdall/playlist.m3u8
#EXTINF:-1 tvg-id="ApnaPunjab.in" tvg-name="Apna Punjab" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Apna Punjab (720p)
http://cdn5.live247stream.com/apnapunjab/tv/playlist.m3u8
#EXTINF:-1 tvg-id="ArtistAloud.in" tvg-name="Artist Aloud" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Music",Artist Aloud
https://live.hungama.com/linear/artist-aloud/playlist.m3u8
#EXTINF:-1 tvg-id="AshrafiChannel.in" tvg-name="Ashrafi Channel" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Ashrafi Channel [Not 24/7]
http://ashrafichannel.livebox.co.in/ashrafivhannelhls/live.m3u8
#EXTINF:-1 tvg-id="AsianetNews.in" tvg-name="Asianet News" tvg-country="IN" tvg-language="Malayalam" tvg-logo="https://i.imgur.com/89LBgkl.png" group-title="News",Asianet News (576p)
@ -150,6 +152,8 @@ http://live.wmncdn.net/desichannel/7e2dd0aed46b70a5c77f4affdb702e4b.sdp/playlist
https://live.wmncdn.net/desichannel/7e2dd0aed46b70a5c77f4affdb702e4b.sdp/mono.m3u8
#EXTINF:-1 tvg-id="DesiPlus.in" tvg-name="Desi Plus" tvg-country="IN" tvg-language="Hindi" tvg-logo="http://mhdtvworld.com/wp-content/uploads/2018/12/desiplustv.jpg" group-title="",Desi Plus (720p)
http://cdn2.live247stream.com/desiplus/tv/playlist.m3u8
#EXTINF:-1 tvg-id="DilSe.in" tvg-name="Dil Se" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Music",Dil Se
https://live.hungama.com/linear/dil-se/playlist.m3u8
#EXTINF:-1 tvg-id="DishaTV.in" tvg-name="Disha TV" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Disha TV
http://xlbor3aadvaj-hls-live.wmncdn.net/disha/stream.stream/playlist.m3u8
#EXTINF:-1 tvg-id="Docubay.in" tvg-name="Docubay" tvg-country="IN" tvg-language="" tvg-logo="https://i.imgur.com/4Bj3h7W.png" group-title="",Docubay
@ -210,6 +214,8 @@ http://103.199.161.254/Content/jeevan/Live/Channel(Jeevan)/index.m3u8
http://159.203.9.134/hls/jhanjar_music/jhanjar_music.m3u8
#EXTINF:-1 tvg-id="JonackTV.in" tvg-name="Jonack TV" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="",Jonack TV (360p)
https://cdn.smartstream.video/smartstream-us/jonakk/jonakk/playlist.m3u8
#EXTINF:-1 tvg-id="KadakHits.in" tvg-name="Kadak Hits" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Music",Kadak Hits
https://live.hungama.com/linear/kadak-hits/playlist.m3u8
#EXTINF:-1 tvg-id="KairaliNews.in" tvg-name="Kairali News" tvg-country="IN" tvg-language="Malayalam" tvg-logo="https://i.imgur.com/hXsVrIh.jpg" group-title="News",Kairali News (576p)
http://103.199.161.254/Content/people/Live/Channel(People)/index.m3u8
#EXTINF:-1 tvg-id="KairaliTV.in" tvg-name="Kairali TV" tvg-country="IN" tvg-language="Malayalam" tvg-logo="https://i.imgur.com/8Di4h6t.png" group-title="Entertainment",Kairali TV (576p)
@ -280,6 +286,8 @@ https://namdhari.tv/live/sbs1.m3u8
https://live.mycast.in/ngtv/d0dbe915091d400bd8ee7f27f0791303.sdp/playlist.m3u8
#EXTINF:-1 tvg-id="NaxtraNews.in" tvg-name="Naxtra News" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="News",Naxtra News
http://wearelive.livebox.co.in/naxatratvhls/Naxatratv.m3u8
#EXTINF:-1 tvg-id="Nazrana.in" tvg-name="Nazrana" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Music",Nazrana
https://live.hungama.com/linear/nazrana/playlist.m3u8
#EXTINF:-1 tvg-id="NDTV24X7.in" tvg-name="NDTV 24X7" tvg-country="IN" tvg-language="Hindi" tvg-logo="http://jiotv.catchup.cdn.jio.com/dare_images/images/NDTV_24x7.png" group-title="News",NDTV 24X7 (480p)
https://ndtv24x7elemarchana.akamaized.net/hls/live/2003678/ndtv24x7/master.m3u8
#EXTINF:-1 tvg-id="NDTVIndia.in" tvg-name="NDTV India" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="",NDTV India (480p)
@ -346,6 +354,8 @@ http://cdn27.live247stream.com/primecanada/247/primecanada/stream1/playlist.m3u8
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
#EXTINF:-1 tvg-id="PunjabiZindabad.in" tvg-name="Punjabi Zindabad" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="",Punjabi Zindabad
http://stream.pztv.online/pztv/playlist.m3u8
#EXTINF:-1 tvg-id="PopPataka.in" tvg-name="Pop Pataka" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Music",Pop Pataka
https://live.hungama.com/linear/pop-pataka/playlist.m3u8
#EXTINF:-1 tvg-id="PuthuyugamTV.in" tvg-name="Puthuyugam TV" tvg-country="IN" tvg-language="Hindi" tvg-logo="http://mhdtvworld.com/wp-content/uploads/2018/11/PUTHUYUGAM.png" group-title="",Puthuyugam TV (576p)
http://103.199.160.85/Content/puthuyugam/Live/Channel(Puthuyugam)/index.m3u8
#EXTINF:-1 tvg-id="QwestClassic.in" tvg-name="Qwest Classic" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Classic",Qwest Classic
@ -400,68 +410,38 @@ http://158.69.124.9:1935/sardaritv/sardaritv/playlist.m3u8
https://satsangtv.sanskargroup.in/satsangtvlive.m3u8
#EXTINF:-1 tvg-id="ServeurTV.in" tvg-name="Serveur TV" tvg-country="IN" tvg-language="English" tvg-logo="" group-title="",Serveur TV
https://a.jsrdn.com/broadcast/060753d37e/+0000/high/c.m3u8
#EXTINF:-1 tvg-id="ShemarooBollywoodClassic.in" tvg-name="Shemaroo Bollywood Classic" tvg-country="IN" tvg-language="Hindi" tvg-logo="https://i.imgur.com/cMJVuIr.jpg" group-title="Movies",Shemaroo Bollywood Classic
https://livechannel.shemaroome.com/linearplayout/02-bollywood-classic/chunklist_1920x1080_cf.m3u8
#EXTINF:-1 tvg-id="ShemarooBollywoodPremier.in" tvg-name="Shemaroo Bollywood Premier" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Shemaroo Bollywood Premier
https://livechannel.shemaroome.com/linearplayout/bollywood-premier-channel/chunklist_1920x1080_cf.m3u8
#EXTINF:-1 tvg-id="ShemarooKids.in" tvg-name="Shemaroo Kids" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Kids",Shemaroo Kids
https://livechannel.shemaroome.com/linearplayout/kids-linear-channel/chunklist_1280x720_cf.m3u8
#EXTINF:-1 tvg-id="ShemarooKids.in" tvg-name="Shemaroo Kids" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="Kids",Shemaroo Kids
https://livechannel.shemaroome.com/linearplayout/kids-linear-channel/chunklist_640x360_cf.m3u8
#EXTINF:-1 tvg-id="ShemarooPunjabiTV.in" tvg-name="Shemaroo Punjabi TV" tvg-country="IN" tvg-language="Hindi" tvg-logo="https://i.imgur.com/t1O83T7.jpg" group-title="",Shemaroo Punjabi TV
https://livechannel.shemaroome.com/linearplayout/punjabi-linear-channel/chunklist_1920x1080_cf.m3u8
#EXTINF:-1 tvg-id="ShirdiLive.in" tvg-name="Shirdi Live" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="",Shirdi Live (720p)
https://cam.live-s.cdn.bitgravity.com/cdn-live/_definst_/cam/live/secure/saibaba/playlist.m3u8?e=0&h=2598445340a35f63eb211f81940d2525
#EXTINF:-1 tvg-id="Shraddha.in" tvg-name="Shraddha" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="",Shraddha (360p)
http://rtmp.smartstream.video:1935/mhoneshradha/mhoneshradha/playlist.m3u8
#EXTINF:-1 tvg-id="SriSankara.in" tvg-name="Sri Sankara" tvg-country="IN" tvg-language="Hindi" tvg-logo="" group-title="Religious",Sri Sankara
https://8noro432dm6g-hls-live.wmncdn.net/Sri/a5518065f47332dad6b509920c827474.sdp/index.m3u8
#EXTINF:-1 tvg-id="Shubhsandesh.in" tvg-name="Shubhsandesh" tvg-country="IN" tvg-language="Hindi" tvg-logo="https://i.imgur.com/ibnqDAD.png" group-title="Religious",Shubhsandesh (576p)
https://6284rn2xr7xv-hls-live.wmncdn.net/shubhsandeshtv1/live123.stream/index.m3u8
#EXTINF:-1 tvg-id="SikhChannel.in" tvg-name="Sikh Channel" tvg-country="IN" tvg-language="Hindi" tvg-logo="https://i.imgur.com/3TZPKGL.png" group-title="",Sikh Channel (576p)
http://fastway.ddns.net:6421/fastway/live8/index.m3u8?token=fastwaytvstreams
#EXTINF:-1 tvg-id="SonyMAX2.in" tvg-name="Sony MAX 2" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony MAX 2
http://208.115.215.42/Sony_Max_HD_02/playlist.m3u8
#EXTINF:-1 tvg-id="SonyMAX2.in" tvg-name="Sony MAX 2" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony MAX 2
http://212.224.98.196:2200/R-EX/HINDI_SONY_MAX_2-in/index.m3u8?token=
#EXTINF:-1 tvg-id="SonyMAXHD.in" tvg-name="Sony MAX HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony MAX HD
http://208.115.215.42/Sony_Max_HD/playlist.m3u8
#EXTINF:-1 tvg-id="SonyMAXHD.in" tvg-name="Sony MAX HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony MAX HD
http://212.224.98.196:2200/R-EX/HINDI_SONY_MAX_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="SonyMovies.in" tvg-name="Sony Movies" tvg-country="IN" tvg-language="English" tvg-logo="" group-title="Movies",Sony Movies
http://51.52.156.22:8888/http/009
#EXTINF:-1 tvg-id="SonySabHD.in" tvg-name="Sony Sab HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony Sab HD
http://208.115.215.42/Sony_Sab_HD/playlist.m3u8
#EXTINF:-1 tvg-id="SonySabHD.in" tvg-name="Sony Sab HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony Sab HD
http://212.224.98.196:2200/R-EX/HINDI_SONY_SAB_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="SonyWAH.in" tvg-name="Sony WAH" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony WAH
https://d2gowxuvx77j6q.cloudfront.net/WAH.m3u8
#EXTINF:-1 tvg-id="SonyYAY.in" tvg-name="Sony YAY" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Sony YAY
https://d20fdzcwhk7szz.cloudfront.net/SONY_YAY.m3u8
#EXTINF:-1 tvg-id="StarBharatHD.in" tvg-name="Star Bharat HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Bharat HD
http://212.224.98.196:2200/R-EX/HINDI_STAR_BHARAT_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="StarCinemaHD.in" tvg-name="Star Cinema HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Cinema HD
http://c0.cdn.trinity-tv.net/stream/nh9u54a7sfnc2hwzxr2zwykwkqm43bgyyzsm68ybbbnjei8kytwcgs3zm5gnw5c6efa5gr3fadzqe686w8gj2eaehrj89wvujvqza3kez78dtknwbbmnqf79yygmqqg7e9pnc3i3bpywjkiqrwke94yf.m3u8
#EXTINF:-1 tvg-id="StarFamilyHD.in" tvg-name="Star Family HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="Family",Star Family HD
http://c0.cdn.trinity-tv.net/stream/zfmjgma9zn46fa797ez9fgkw7msh9mj4tppspg23gey6mmx5fqiy7ky3jqx4uhgsfsrd8r76si8ykb2anw9442g4qkq5fzpdvwdqf5te24ixu9zrx3aesm9fzt59q5y2s8qwgbqhvf6d3z5bjy3qb2t4.m3u8
#EXTINF:-1 tvg-id="StarGoldHD.in" tvg-name="Star Gold HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Gold HD
http://212.224.98.196:2200/R-EX/HINDI_STAR_GOLD_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="StarJalsha.in" tvg-name="Star Jalsha" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Jalsha
http://212.224.98.196:2200/R-EX/BENGALI_STAR_JALSHA_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="StarMaaHD.in" tvg-name="Star Maa HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Maa HD
http://212.224.98.196:2200/R-EX/TELUGU_STAR_MAA_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="StarMaaMoviesHD.in" tvg-name="Star Maa Movies HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Maa Movies HD
http://212.224.98.196:2200/R-EX/TELUGU_STAR_MAA_MOVIES_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="StarPlusHD.in" tvg-name="Star Plus HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Plus HD
http://208.115.215.42/Utsav_Plus_HD/playlist.m3u8
#EXTINF:-1 tvg-id="StarPlusHD.in" tvg-name="Star Plus HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Plus HD
http://212.224.98.196:2200/R-EX/HINDI_STAR_PLUS_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="StarUtsav.in" tvg-name="Star Utsav" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Utsav
https://dolv5imquuojb.cloudfront.net/ST_UTSAV.m3u8
#EXTINF:-1 tvg-id="StarVijayHD.in" tvg-name="Star Vijay HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Vijay HD
http://185.180.221.194:8278/streams/d/starvijay/playlist.m3u8
#EXTINF:-1 tvg-id="StarVijayUK.in" tvg-name="Star Vijay UK" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star Vijay UK
http://fr1.zecast.com:1935/star-live/vijay.stream/playlist.m3u8
#EXTINF:-1 tvg-id="StarWorldHD.in" tvg-name="Star World HD" tvg-country="IN" tvg-language="" tvg-logo="" group-title="",Star World HD
http://212.224.98.196:2200/R-EX/ENGLISH_STAR_WORLD_HD-in/index.m3u8?token=
#EXTINF:-1 tvg-id="SteelbirdMusic.in" tvg-name="Steelbird Music" tvg-country="IN" tvg-language="" tvg-logo="https://i.imgur.com/5r4gCMh.jpg" group-title="Music",Steelbird Music (720p) [Not 24/7]
http://cdn25.live247stream.com/steelbirdmusic/tv/playlist.m3u8
#EXTINF:-1 tvg-id="SuryaTV.in" tvg-name="Surya TV" tvg-country="IN" tvg-language="Malayalam" tvg-logo="" group-title="",Surya TV

@ -39,16 +39,16 @@ http://159.69.58.154/ekran/ekrantv.m3u8
https://faraztv.net/hls/stream.m3u8
#EXTINF:-1 tvg-id="Film1.ir" tvg-name="Film 1" tvg-country="IR" tvg-language="Persian" tvg-logo="" group-title="Movies",Film 1
http://159.69.58.154/film1/film1tv.m3u8
#EXTINF:-1 tvg-id="GEM24b.ir" tvg-name="GEM 24b" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/24b.png" group-title="Entertainment",GEM 24b [Geo-blocked]
#EXTINF:-1 tvg-id="GEM24b.ir" tvg-name="GEM 24b" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/24b.png" group-title="Music",GEM 24b [Geo-blocked]
#EXTVLCOPT:http-referrer=https://www.gemonline.tv/en-US/Live/Index?channelname=24b
https://d2e40kvaojifd6.cloudfront.net/stream/24b/playlist.m3u8
#EXTINF:-1 tvg-id="GEMAcademy.ir" tvg-name="GEM Academy" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/gemusa.png" group-title="Movies",GEM Academy [Geo-blocked]
#EXTVLCOPT:http-referrer=https://www.gemonline.tv/en-US/Live/Index?channelname=gemusa
https://d2e40kvaojifd6.cloudfront.net/stream/gem_usa/playlist.m3u8
#EXTINF:-1 tvg-id="GEMArabia.ir" tvg-name="GEM Arabia" tvg-country="IR" tvg-language="Arabic" tvg-logo="https://www.gemonline.tv/Assets/channels-box/gem_arabia.png" group-title="Entertainment",GEM Arabia [Geo-blocked]
#EXTINF:-1 tvg-id="GEMArabia.ir" tvg-name="GEM Arabia" tvg-country="IR" tvg-language="Arabic" tvg-logo="https://www.gemonline.tv/Assets/channels-box/gem_arabia.png" group-title="Music",GEM Arabia [Geo-blocked]
#EXTVLCOPT:http-referrer=https://www.gemonline.tv/en-US/Live/Index?channelname=gem_arabia
https://d2e40kvaojifd6.cloudfront.net/stream/gem_arabia/playlist.m3u8
#EXTINF:-1 tvg-id="GEMAZ.ir" tvg-name="GEM AZ" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/gem_az.png" group-title="Entertainment",GEM AZ [Geo-blocked]
#EXTINF:-1 tvg-id="GEMAZ.ir" tvg-name="GEM AZ" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/gem_az.png" group-title="Music",GEM AZ [Geo-blocked]
#EXTVLCOPT:http-referrer=https://www.gemonline.tv/en-US/Live/Index?channelname=gem_az
https://d2e40kvaojifd6.cloudfront.net/stream/gem_az/playlist.m3u8
#EXTINF:-1 tvg-id="GEMBollywood.ir" tvg-name="GEM Bollywood" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/gembollywood.png" group-title="Movies",GEM Bollywood [Geo-blocked]
@ -87,7 +87,7 @@ https://d2e40kvaojifd6.cloudfront.net/stream/gem_life/playlist.m3u8
#EXTINF:-1 tvg-id="GEMMaxx.ir" tvg-name="GEM Maxx" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/maxx.png?14" group-title="Entertainment",GEM Maxx [Geo-blocked]
#EXTVLCOPT:http-referrer=https://www.gemonline.tv/en-US/Live/Index?channelname=maxx
https://d2e40kvaojifd6.cloudfront.net/stream/maxx/playlist.m3u8
#EXTINF:-1 tvg-id="GEMMifa.ir" tvg-name="GEM Mifa" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/mifa.png" group-title="Entertainment",GEM Mifa [Geo-blocked]
#EXTINF:-1 tvg-id="GEMMifa.ir" tvg-name="GEM Mifa" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/mifa.png" group-title="Music",GEM Mifa [Geo-blocked]
#EXTVLCOPT:http-referrer=https://www.gemonline.tv/en-US/Live/Index?channelname=mifa
https://d2e40kvaojifd6.cloudfront.net/stream/mifa/playlist.m3u8
#EXTINF:-1 tvg-id="GEMModernEconomy.ir" tvg-name="GEM ModernEconomy" tvg-country="IR" tvg-language="Persian" tvg-logo="https://www.gemonline.tv/Assets/channels-box/modern_economy.png" group-title="Documentary",GEM Modern Economy [Geo-blocked]
@ -119,9 +119,9 @@ https://d2e40kvaojifd6.cloudfront.net/stream/gem_travel/playlist.m3u8
https://d2e40kvaojifd6.cloudfront.net/stream/gem_tv/playlist.m3u8
#EXTINF:-1 tvg-id="HastiTV.ir" tvg-name="Hasti TV" tvg-country="IR" tvg-language="Persian" tvg-logo="" group-title="",Hasti TV
https://live.hastitv.com/hls/livetv.m3u8
#EXTINF:-1 tvg-id="HispanTVIran.ir" tvg-name="Hispan TV Iran" tvg-country="IR" tvg-language="Spanish" tvg-logo="" group-title="",Hispan TV Iran
#EXTINF:-1 tvg-id="Hispantv.ir" tvg-name="HispanTV" tvg-country="IR;ES;LATAM" tvg-language="Spanish" tvg-logo="https://en.wikipedia.org/wiki/File:HispanTv_logo.svg" group-title="News",HispanTV
https://live1.presstv.ir/live/hispan.m3u8
#EXTINF:-1 tvg-id="Hispantv.ir" tvg-name="HispanTV" tvg-country="IR" tvg-language="Spanish" tvg-logo="https://en.wikipedia.org/wiki/File:HispanTv_logo.svg" group-title="News",HispanTV (480p)
#EXTINF:-1 tvg-id="Hispantv.ir" tvg-name="HispanTV" tvg-country="IR;ES;LATAM" tvg-language="Spanish" tvg-logo="https://en.wikipedia.org/wiki/File:HispanTv_logo.svg" group-title="News",HispanTV (480p)
https://live.presstv.ir/live/smil:live.smil/playlist.m3u8
#EXTINF:-1 tvg-id="HodHodTV.ir" tvg-name="HodHod TV" tvg-country="IR" tvg-language="" tvg-logo="https://i.imgur.com/TMxCgX1.jpg" group-title="",HodHod TV
http://51.210.199.12/hls/stream.m3u8

@ -1,14 +1,14 @@
const { program } = require('commander')
const parser = require('./parser')
const utils = require('./utils')
const axios = require('axios')
const ProgressBar = require('progress')
const axios = require('axios')
const https = require('https')
const chalk = require('chalk')
const parser = require('./helpers/parser')
const utils = require('./helpers/utils')
const log = require('./helpers/log')
program
.usage('[OPTIONS]...')
.option('-d, --debug', 'Debug mode')
.option('-c, --country <country>', 'Comma-separated list of country codes', '')
.option('-e, --exclude <exclude>', 'Comma-separated list of country codes to be excluded', '')
.option('--delay <delay>', 'Delay between parser requests', 1000)
@ -16,8 +16,8 @@ program
.parse(process.argv)
const config = program.opts()
const offlineStatusCodes = [404, 410, 500, 501]
const ignore = ['Geo-blocked', 'Not 24/7']
const instance = axios.create({
timeout: config.timeout,
maxContentLength: 200000,
@ -29,60 +29,39 @@ const instance = axios.create({
}
})
const ignore = ['Geo-blocked', 'Not 24/7']
const stats = { broken: 0 }
let broken = 0
async function main() {
console.info(`\nStarting...`)
console.time('Done in')
if (config.debug) {
console.info(chalk.yellow(`INFO: Debug mode enabled\n`))
}
const playlists = parseIndex()
log.start()
for (const playlist of playlists) {
await loadPlaylist(playlist.url).then(checkStatus).then(savePlaylist).then(done)
}
finish()
}
function parseIndex() {
console.info(`Parsing 'index.m3u'...`)
log.print(`Parsing 'index.m3u'...`)
let playlists = parser.parseIndex()
playlists = utils.filterPlaylists(playlists, config.country, config.exclude)
console.info(`Found ${playlists.length} playlist(s)\n`)
return playlists
}
for (const playlist of playlists) {
await parser
.parsePlaylist(playlist.url)
.then(checkStatus)
.then(p => p.save())
}
async function loadPlaylist(url) {
console.info(`Processing '${url}'...`)
return parser.parsePlaylist(url)
log.finish()
}
async function checkStatus(playlist) {
let bar
if (!config.debug) {
bar = new ProgressBar(' Testing: [:bar] :current/:total (:percent) ', {
total: playlist.channels.length
})
}
const results = []
let bar = new ProgressBar(`Checking '${playlist.url}': [:bar] :current/:total (:percent) `, {
total: playlist.channels.length
})
const channels = []
const total = playlist.channels.length
for (const [index, channel] of playlist.channels.entries()) {
const current = index + 1
const counter = chalk.gray(`[${current}/${total}]`)
if (bar) bar.tick()
bar.tick()
if (
(channel.status && ignore.map(i => i.toLowerCase()).includes(channel.status.toLowerCase())) ||
(!channel.url.startsWith('http://') && !channel.url.startsWith('https://'))
) {
results.push(channel)
if (config.debug) {
console.info(` ${counter} ${chalk.green('online')} ${chalk.white(channel.url)}`)
}
channels.push(channel)
} else {
const CancelToken = axios.CancelToken
const source = CancelToken.source()
@ -94,55 +73,27 @@ async function checkStatus(playlist) {
.get(channel.url, { cancelToken: source.token })
.then(() => {
clearTimeout(timeout)
results.push(channel)
if (config.debug) {
console.info(` ${counter} ${chalk.green('online')} ${chalk.white(channel.url)}`)
}
channels.push(channel)
})
.then(utils.sleep(config.delay))
.catch(err => {
clearTimeout(timeout)
if (err.response && offlineStatusCodes.includes(err.response.status)) {
if (config.debug) {
console.info(` ${counter} ${chalk.red('offline')} ${chalk.white(channel.url)}`)
}
stats.broken++
broken++
} else {
results.push(channel)
if (config.debug) {
console.info(` ${counter} ${chalk.green('online')} ${chalk.white(channel.url)}`)
}
channels.push(channel)
}
})
}
}
playlist.channels = results
return playlist
}
async function savePlaylist(playlist) {
const original = utils.readFile(playlist.url)
const output = playlist.toString({ raw: true })
if (original === output) {
console.info(`No changes have been made.`)
return false
} else {
utils.createFile(playlist.url, output)
console.info(`Playlist has been updated. Removed ${stats.broken} links.`)
if (playlist.channels.length !== channels.length) {
log.print(`File '${playlist.url}' has been updated\n`)
playlist.channels = channels
playlist.updated = true
}
return true
}
async function done() {
console.info(` `)
}
function finish() {
console.timeEnd('Done in')
return playlist
}
main()

@ -0,0 +1,110 @@
const { program } = require('commander')
const ProgressBar = require('progress')
const axios = require('axios')
const https = require('https')
const parser = require('./helpers/parser')
const utils = require('./helpers/utils')
const log = require('./helpers/log')
program
.usage('[OPTIONS]...')
.option('-c, --country <country>', 'Comma-separated list of country codes', '')
.option('-e, --exclude <exclude>', 'Comma-separated list of country codes to be excluded', '')
.option('--delay <delay>', 'Delay between parser requests', 1000)
.option('--timeout <timeout>', 'Set timeout for each request', 5000)
.parse(process.argv)
const config = program.opts()
const instance = axios.create({
timeout: config.timeout,
maxContentLength: 200000,
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
})
async function main() {
log.start()
log.print(`Parsing 'index.m3u'...\n`)
let playlists = parser.parseIndex()
playlists = utils
.filterPlaylists(playlists, config.country, config.exclude)
.filter(i => i.url !== 'channels/unsorted.m3u')
for (const playlist of playlists) {
await parser
.parsePlaylist(playlist.url)
.then(detectResolution)
.then(p => p.save())
}
log.finish()
}
async function detectResolution(playlist) {
const channels = []
const bar = new ProgressBar(`Processing '${playlist.url}': [:bar] :current/:total (:percent) `, {
total: playlist.channels.length
})
let updated = false
for (const channel of playlist.channels) {
bar.tick()
if (!channel.resolution.height) {
const CancelToken = axios.CancelToken
const source = CancelToken.source()
const timeout = setTimeout(() => {
source.cancel()
}, config.timeout)
const response = await instance
.get(channel.url, { cancelToken: source.token })
.then(res => {
clearTimeout(timeout)
return res
})
.then(utils.sleep(config.delay))
.catch(err => {
clearTimeout(timeout)
})
if (response && response.status === 200) {
if (/^#EXTM3U/.test(response.data)) {
const resolution = parseResolution(response.data)
if (resolution) {
channel.resolution = resolution
updated = true
}
}
}
}
channels.push(channel)
}
if (updated) {
log.print(`File '${playlist.url}' has been updated\n`)
playlist.channels = channels
playlist.updated = true
}
return playlist
}
function parseResolution(string) {
const regex = /RESOLUTION=(\d+)x(\d+)/gm
const match = string.matchAll(regex)
const arr = Array.from(match).map(m => ({
width: parseInt(m[1]),
height: parseInt(m[2])
}))
return arr.length
? arr.reduce(function (prev, current) {
return prev.height > current.height ? prev : current
})
: undefined
}
main()

@ -1,67 +1,43 @@
const parser = require('./parser')
const utils = require('./utils')
const blacklist = require('./blacklist.json')
const blacklist = require('./helpers/blacklist.json')
const parser = require('./helpers/parser')
const log = require('./helpers/log')
async function main() {
const playlists = parseIndex()
log.start()
log.print(`Parsing 'index.m3u'...`)
const playlists = parser.parseIndex()
for (const playlist of playlists) {
await loadPlaylist(playlist.url).then(removeBlacklisted).then(savePlaylist).then(done)
log.print(`\nProcessing '${playlist.url}'...`)
await parser
.parsePlaylist(playlist.url)
.then(removeBlacklisted)
.then(p => p.save())
}
finish()
}
function parseIndex() {
console.info(`Parsing 'index.m3u'...`)
let playlists = parser.parseIndex()
console.info(`Found ${playlists.length} playlist(s)\n`)
return playlists
}
async function loadPlaylist(url) {
console.info(`Processing '${url}'...`)
return parser.parsePlaylist(url)
log.print('\n')
log.finish()
}
async function removeBlacklisted(playlist) {
console.info(` Looking for blacklisted channels...`)
playlist.channels = playlist.channels.filter(channel => {
return !blacklist.find(i => {
const channelName = channel.name.toLowerCase()
return (
(i.name.toLowerCase() === channelName ||
i.aliases.map(i => i.toLowerCase()).includes(channelName)) &&
i.country === channel.filename
)
const channels = playlist.channels.filter(channel => {
return !blacklist.find(item => {
const hasSameName =
item.name.toLowerCase() === channel.name.toLowerCase() ||
item.aliases.map(alias => alias.toLowerCase()).includes(channel.name.toLowerCase())
const fromSameCountry = channel.countries.find(c => c.code === item.country)
return hasSameName && fromSameCountry
})
})
return playlist
}
async function savePlaylist(playlist) {
console.info(` Saving playlist...`)
const original = utils.readFile(playlist.url)
const output = playlist.toString({ raw: true })
if (original === output) {
console.info(`No changes have been made.`)
return false
} else {
utils.createFile(playlist.url, output)
console.info(`Playlist has been updated.`)
if (playlist.channels.length !== channels.length) {
log.print(`updated`)
playlist.channels = channels
playlist.updated = true
}
return true
}
async function done() {
console.info(` `)
}
function finish() {
console.info('Done.')
return playlist
}
main()

@ -1,152 +1,55 @@
const { program } = require('commander')
const parser = require('./parser')
const utils = require('./utils')
const axios = require('axios')
const ProgressBar = require('progress')
const https = require('https')
program
.usage('[OPTIONS]...')
.option('-d, --debug', 'Debug mode')
.option('-r --resolution', 'Parse stream resolution')
.option('-c, --country <country>', 'Comma-separated list of country codes', '')
.option('-e, --exclude <exclude>', 'Comma-separated list of country codes to be excluded', '')
.option('--delay <delay>', 'Delay between parser requests', 1000)
.option('--timeout <timeout>', 'Set timeout for each request', 5000)
.parse(process.argv)
const config = program.opts()
const instance = axios.create({
timeout: config.timeout,
maxContentLength: 200000,
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
})
const parser = require('./helpers/parser')
const utils = require('./helpers/utils')
const file = require('./helpers/file')
const log = require('./helpers/log')
async function main() {
console.info('Starting...')
console.time('Done in')
const playlists = parseIndex()
log.start()
log.print(`Parsing 'index.m3u'...`)
let playlists = parser.parseIndex().filter(i => i.url !== 'channels/unsorted.m3u')
for (const playlist of playlists) {
await loadPlaylist(playlist.url)
.then(sortChannels)
.then(detectResolution)
.then(savePlaylist)
.then(done)
}
finish()
}
function parseIndex() {
console.info(`\nParsing 'index.m3u'...`)
let playlists = parser.parseIndex()
playlists = utils
.filterPlaylists(playlists, config.country, config.exclude)
.filter(i => i.url !== 'channels/unsorted.m3u')
console.info(`Found ${playlists.length} playlist(s)\n`)
return playlists
}
async function loadPlaylist(url) {
console.info(`Processing '${url}'...`)
return parser.parsePlaylist(url)
}
log.print(`\nProcessing '${playlist.url}'...`)
await parser
.parsePlaylist(playlist.url)
.then(formatPlaylist)
.then(playlist => {
if (file.read(playlist.url) !== playlist.toString()) {
log.print('updated')
playlist.updated = true
}
async function sortChannels(playlist) {
console.info(` Sorting channels...`)
playlist.channels = utils.sortBy(playlist.channels, ['name', 'url'])
playlist.save()
})
}
return playlist
log.print('\n')
log.finish()
}
async function detectResolution(playlist) {
if (!config.resolution) return playlist
console.log(' Detecting resolution...')
const bar = new ProgressBar(' Progress: [:bar] :current/:total (:percent) ', {
total: playlist.channels.length
})
const results = []
async function formatPlaylist(playlist) {
for (const channel of playlist.channels) {
bar.tick()
if (!channel.resolution.height) {
const CancelToken = axios.CancelToken
const source = CancelToken.source()
const timeout = setTimeout(() => {
source.cancel()
}, config.timeout)
const response = await instance
.get(channel.url, { cancelToken: source.token })
.then(res => {
clearTimeout(timeout)
return res
})
.then(utils.sleep(config.delay))
.catch(err => {
clearTimeout(timeout)
})
if (response && response.status === 200) {
if (/^#EXTM3U/.test(response.data)) {
const resolution = parseResolution(response.data)
if (resolution) {
channel.resolution = resolution
}
}
}
const code = file.getBasename(playlist.url)
// add missing tvg-name
if (!channel.tvg.name && code !== 'unsorted' && channel.name) {
channel.tvg.name = channel.name.replace(/\"/gi, '')
}
results.push(channel)
// add missing tvg-id
if (!channel.tvg.id && code !== 'unsorted' && channel.tvg.name) {
const id = utils.name2id(channel.tvg.name)
channel.tvg.id = id ? `${id}.${code}` : ''
}
// add missing country
if (!channel.countries.length) {
const name = utils.code2name(code)
channel.countries = name ? [{ code, name }] : []
channel.tvg.country = channel.countries.map(c => c.code.toUpperCase()).join(';')
}
// update group-title
channel.group.title = channel.category
}
playlist.channels = results
return playlist
}
function parseResolution(string) {
const regex = /RESOLUTION=(\d+)x(\d+)/gm
const match = string.matchAll(regex)
const arr = Array.from(match).map(m => ({
width: parseInt(m[1]),
height: parseInt(m[2])
}))
return arr.length
? arr.reduce(function (prev, current) {
return prev.height > current.height ? prev : current
})
: undefined
}
async function savePlaylist(playlist) {
const original = utils.readFile(playlist.url)
const output = playlist.toString()
if (original === output) {
console.info(`No changes have been made.`)
return false
} else {
utils.createFile(playlist.url, output)
console.info(`Playlist has been updated.`)
}
return true
}
async function done() {
console.info(` `)
}
function finish() {
console.timeEnd('Done in')
}
main()

@ -1,11 +1,11 @@
const db = require('./db')
const utils = require('./utils')
const file = require('./helpers/file')
const log = require('./helpers/log')
const db = require('./helpers/db')
const ROOT_DIR = './.gh-pages'
db.load()
function main() {
async function main() {
await loadDatabase()
createRootDirectory()
createNoJekyllFile()
generateIndex()
@ -16,51 +16,56 @@ function main() {
generateCountries()
generateLanguages()
generateChannelsJson()
finish()
showResults()
}
async function loadDatabase() {
log.print('Loading database...\n')
await db.load()
}
function createRootDirectory() {
console.log('Creating .gh-pages folder...')
utils.createDir(ROOT_DIR)
log.print('Creating .gh-pages folder...\n')
file.createDir(ROOT_DIR)
}
function createNoJekyllFile() {
console.log('Creating .nojekyll...')
utils.createFile(`${ROOT_DIR}/.nojekyll`)
log.print('Creating .nojekyll...\n')
file.create(`${ROOT_DIR}/.nojekyll`)
}
function generateIndex() {
console.log('Generating index.m3u...')
log.print('Generating index.m3u...\n')
const filename = `${ROOT_DIR}/index.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
const nsfwFilename = `${ROOT_DIR}/index.nsfw.m3u`
utils.createFile(nsfwFilename, '#EXTM3U\n')
file.create(nsfwFilename, '#EXTM3U\n')
const channels = db.channels.sortBy(['name', 'url']).removeDuplicates().get()
for (const channel of channels) {
if (!channel.isNSFW()) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
utils.appendToFile(nsfwFilename, channel.toString())
file.append(nsfwFilename, channel.toString())
}
}
function generateCategoryIndex() {
console.log('Generating index.category.m3u...')
log.print('Generating index.category.m3u...\n')
const filename = `${ROOT_DIR}/index.category.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
const channels = db.channels.sortBy(['category', 'name', 'url']).removeDuplicates().get()
for (const channel of channels) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
}
function generateCountryIndex() {
console.log('Generating index.country.m3u...')
log.print('Generating index.country.m3u...\n')
const filename = `${ROOT_DIR}/index.country.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
for (const country of [{ code: 'undefined' }, ...db.countries.sortBy(['name']).all()]) {
const channels = db.channels
@ -69,21 +74,21 @@ function generateCountryIndex() {
.removeDuplicates()
.get()
for (const channel of channels) {
const category = channel.category
const groupTitle = channel.group.title
const nsfw = channel.isNSFW()
channel.category = country.name || ''
channel.group.title = country.name || ''
if (!nsfw) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
channel.category = category
channel.group.title = groupTitle
}
}
}
function generateLanguageIndex() {
console.log('Generating index.language.m3u...')
log.print('Generating index.language.m3u...\n')
const filename = `${ROOT_DIR}/index.language.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
for (const language of [{ code: 'undefined' }, ...db.languages.sortBy(['name']).all()]) {
const channels = db.channels
@ -92,25 +97,25 @@ function generateLanguageIndex() {
.removeDuplicates()
.get()
for (const channel of channels) {
const category = channel.category
const groupTitle = channel.group.title
const nsfw = channel.isNSFW()
channel.category = language.name || ''
channel.group.title = language.name || ''
if (!nsfw) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
channel.category = category
channel.group.title = groupTitle
}
}
}
function generateCategories() {
console.log(`Generating /categories...`)
log.print(`Generating /categories...\n`)
const outputDir = `${ROOT_DIR}/categories`
utils.createDir(outputDir)
file.createDir(outputDir)
for (const category of [...db.categories.all(), { id: 'other' }]) {
const filename = `${outputDir}/${category.id}.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
const channels = db.channels
.sortBy(['name', 'url'])
@ -118,19 +123,19 @@ function generateCategories() {
.removeDuplicates()
.get()
for (const channel of channels) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
}
}
function generateCountries() {
console.log(`Generating /countries...`)
log.print(`Generating /countries...\n`)
const outputDir = `${ROOT_DIR}/countries`
utils.createDir(outputDir)
file.createDir(outputDir)
for (const country of [...db.countries.all(), { code: 'undefined' }]) {
const filename = `${outputDir}/${country.code}.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
const channels = db.channels
.sortBy(['name', 'url'])
@ -139,20 +144,20 @@ function generateCountries() {
.get()
for (const channel of channels) {
if (!channel.isNSFW()) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
}
}
}
function generateLanguages() {
console.log(`Generating /languages...`)
log.print(`Generating /languages...\n`)
const outputDir = `${ROOT_DIR}/languages`
utils.createDir(outputDir)
file.createDir(outputDir)
for (const language of [...db.languages.all(), { code: 'undefined' }]) {
const filename = `${outputDir}/${language.code}.m3u`
utils.createFile(filename, '#EXTM3U\n')
file.create(filename, '#EXTM3U\n')
const channels = db.channels
.sortBy(['name', 'url'])
@ -161,25 +166,25 @@ function generateLanguages() {
.get()
for (const channel of channels) {
if (!channel.isNSFW()) {
utils.appendToFile(filename, channel.toString())
file.append(filename, channel.toString())
}
}
}
}
function generateChannelsJson() {
console.log('Generating channels.json...')
log.print('Generating channels.json...\n')
const filename = `${ROOT_DIR}/channels.json`
const channels = db.channels
.sortBy(['name', 'url'])
.get()
.map(c => c.toObject())
utils.createFile(filename, JSON.stringify(channels))
file.create(filename, JSON.stringify(channels))
}
function finish() {
console.log(
`\nTotal: ${db.channels.count()} channels, ${db.countries.count()} countries, ${db.languages.count()} languages, ${db.categories.count()} categories.`
function showResults() {
log.print(
`Total: ${db.channels.count()} channels, ${db.countries.count()} countries, ${db.languages.count()} languages, ${db.categories.count()} categories.\n`
)
}

@ -0,0 +1,145 @@
const categories = require('./categories')
const utils = require('./utils')
const file = require('./file')
const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
const nsfwCategories = categories.filter(c => c.nsfw).map(c => c.name)
module.exports = class Channel {
constructor(data) {
this.raw = data.raw
this.tvg = data.tvg
this.http = data.http
this.url = data.url
this.logo = data.tvg.logo
this.group = data.group
this.name = this.parseName(data.name)
this.status = this.parseStatus(data.name)
this.resolution = this.parseResolution(data.name)
this.category = this.parseCategory(data.group.title)
this.countries = this.parseCountries(data.tvg.country)
this.languages = this.parseLanguages(data.tvg.language)
}
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 match = title.match(/\[(.*)\]/i)
return match ? match[1] : null
}
parseResolution(title) {
const match = title.match(/\((\d+)P\)/i)
const height = match ? parseInt(match[1]) : null
return { width: null, height }
}
parseCategory(string) {
const category = categories.find(c => c.id === string.toLowerCase())
if (!category) return ''
return category.name
}
parseCountries(string) {
const list = string.split(';')
return list
.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
}, [])
.map(code => {
const name = code ? utils.code2name(code) : null
if (!name) return null
return { code: code.toLowerCase(), name }
})
.filter(c => c)
}
parseLanguages(string) {
const list = string.split(';')
return list
.map(name => {
const code = name ? utils.language2code(name) : null
if (!code) return null
return { code, name }
})
.filter(l => l)
}
isSFW() {
return sfwCategories.includes(this.category)
}
isNSFW() {
return nsfwCategories.includes(this.category)
}
getInfo() {
let info = `-1 tvg-id="${this.tvg.id}" tvg-name="${this.tvg.name}" tvg-country="${this.tvg.country}" tvg-language="${this.tvg.language}" tvg-logo="${this.logo}"`
info += ` group-title="${this.group.title}",${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 info
}
toString(raw = false) {
if (raw) return this.raw + '\n'
return '#EXTINF:' + this.getInfo() + '\n' + this.url + '\n'
}
toObject() {
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
}
}
}
}

@ -0,0 +1,37 @@
const Channel = require('./Channel')
const file = require('./file')
module.exports = class Playlist {
constructor({ header, items, url, name, country }) {
this.url = url
this.name = name
this.country = country
this.header = header
this.channels = items.map(item => new Channel(item)).filter(channel => channel.url)
this.updated = false
}
toString(options = {}) {
const config = { raw: false, ...options }
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(config.raw)
}
return output
}
save() {
if (this.updated) {
file.create(this.url, this.toString())
}
}
}

@ -2,14 +2,12 @@ const categories = require('./categories')
const parser = require('./parser')
const utils = require('./utils')
const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
const db = {}
db.load = function () {
db.load = async function () {
const items = parser.parseIndex()
for (const item of items) {
const playlist = parser.parsePlaylist(item.url)
const playlist = await parser.parsePlaylist(item.url)
db.playlists.add(playlist)
for (const channel of playlist.channels) {
db.channels.add(channel)
@ -107,9 +105,6 @@ db.channels = {
all() {
return this.list
},
sfw() {
return this.list.filter(i => sfwCategories.includes(i.category))
},
forCountry(country) {
this.filter = {
field: 'countries',

@ -0,0 +1,38 @@
const markdownInclude = require('markdown-include')
const path = require('path')
const fs = require('fs')
const rootPath = path.resolve(__dirname) + '/../../'
const file = {}
file.getBasename = function (filename) {
return path.basename(filename, path.extname(filename))
}
file.getFilename = function (filename) {
return path.parse(filename).name
}
file.createDir = function (dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
}
file.read = function (filename) {
return fs.readFileSync(rootPath + filename, { encoding: 'utf8' })
}
file.append = function (filename, data) {
fs.appendFileSync(rootPath + filename, data)
}
file.create = function (filename, data = '') {
fs.writeFileSync(rootPath + filename, data)
}
file.compileMarkdown = function (filename) {
markdownInclude.compileFiles(rootPath + filename)
}
module.exports = file

@ -0,0 +1,16 @@
const log = {}
log.print = function (string) {
process.stdout.write(string)
}
log.start = function () {
this.print('Starting...\n')
console.time('Done in')
}
log.finish = function () {
console.timeEnd('Done in')
}
module.exports = log

@ -0,0 +1,24 @@
const playlistParser = require('iptv-playlist-parser')
const Playlist = require('./Playlist')
const utils = require('./utils')
const file = require('./file')
const parser = {}
parser.parseIndex = function () {
const content = file.read('index.m3u')
const result = playlistParser.parse(content)
return result.items
}
parser.parsePlaylist = async function (url) {
const content = file.read(url)
const result = playlistParser.parse(content)
const name = file.getFilename(url)
const country = utils.code2name(name)
return new Playlist({ header: result.header, items: result.items, url, country, name })
}
module.exports = parser

@ -1,21 +1,15 @@
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('@freearhey/iso-639-3')
const transliteration = require('transliteration')
const regions = require('./regions')
const iso6393 = require('@freearhey/iso-639-3')
const categories = require('./categories')
const regions = require('./regions')
const utils = {}
const intlDisplayNames = new Intl.DisplayNames(['en'], {
style: 'narrow',
type: 'region'
})
const utils = {}
utils.name2id = function (name) {
return transliteration
.transliterate(name)
@ -66,36 +60,29 @@ utils.sortBy = function (arr, fields) {
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
}
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.getBasename = function (filename) {
return path.basename(filename, path.extname(filename))
utils.escapeStringRegexp = function (scring) {
return escapeStringRegexp(string)
}
utils.sleep = function (ms) {
return function (x) {
return new Promise(resolve => setTimeout(() => resolve(x), ms))
}
}
utils.removeProtocol = function (string) {
return string.replace(/(^\w+:|^)\/\//, '')
}
utils.filterPlaylists = function (arr, include = '', exclude = '') {
@ -114,75 +101,4 @@ utils.filterPlaylists = function (arr, include = '', exclude = '') {
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.sleep = function (ms) {
return function (x) {
return new Promise(resolve => setTimeout(() => resolve(x), ms))
}
}
utils.removeProtocol = function (string) {
return string.replace(/(^\w+:|^)\/\//, '')
}
module.exports = utils

@ -1,244 +0,0 @@
const playlistParser = require('iptv-playlist-parser')
const utils = require('./utils')
const categories = require('./categories')
const path = require('path')
const sfwCategories = categories.filter(c => !c.nsfw).map(c => c.name)
const nsfwCategories = categories.filter(c => c.nsfw).map(c => c.name)
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)
const name = path.parse(filename).name
const country = utils.code2name(name)
return new Playlist({ header: result.header, items: result.items, url: filename, country, name })
}
class Playlist {
constructor({ header, items, url, name, country }) {
this.url = url
this.name = name
this.country = country
this.header = header
this.channels = items
.map(item => new Channel({ data: item, header, sourceUrl: url }))
.filter(channel => channel.url)
}
toString(options = {}) {
const config = { raw: false, ...options }
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(config.raw)
}
return output
}
}
class Channel {
constructor({ data, header, sourceUrl }) {
this.parseData(data)
this.filename = utils.getBasename(sourceUrl)
if (!this.countries.length) {
const countryName = utils.code2name(this.filename)
this.countries = countryName ? [{ code: this.filename, name: countryName }] : []
this.tvg.country = this.countries.map(c => c.code.toUpperCase()).join(';')
}
}
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)
this.raw = data.raw
}
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.url ? this.tvg.url : ''
}
get tvgId() {
if (this.tvg.id) {
return this.tvg.id
} else if (this.filename !== 'unsorted') {
const id = utils.name2id(this.tvgName)
return id ? `${id}.${this.filename}` : ''
}
return ''
}
get tvgName() {
if (this.tvg.name) {
return this.tvg.name
} else if (this.filename !== 'unsorted') {
return this.name.replace(/\"/gi, '')
}
return ''
}
getInfo() {
this.tvg.country = this.tvg.country.toUpperCase()
let info = `-1 tvg-id="${this.tvgId}" tvg-name="${this.tvgName}" tvg-country="${this.tvg.country}" tvg-language="${this.tvg.language}" tvg-logo="${this.logo}"`
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 info
}
toString(raw = false) {
if (raw) return this.raw + '\n'
return '#EXTINF:' + this.getInfo() + '\n' + this.url + '\n'
}
toObject() {
return {
name: this.name,
logo: this.logo || null,
url: this.url,
category: this.category || null,
languages: this.languages,
countries: this.countries,
tvg: {
id: this.tvgId || null,
name: this.tvgName || null,
url: this.tvgUrl || null
}
}
}
isSFW() {
return sfwCategories.includes(this.category)
}
isNSFW() {
return nsfwCategories.includes(this.category)
}
}
module.exports = parser

@ -1,52 +1,42 @@
const parser = require('./parser')
const utils = require('./utils')
const parser = require('./helpers/parser')
const utils = require('./helpers/utils')
const log = require('./helpers/log')
let globalBuffer = []
async function main() {
const playlists = parseIndex()
log.start()
log.print(`Parsing 'index.m3u'...`)
const playlists = parser.parseIndex().filter(i => i.url !== 'channels/unsorted.m3u')
for (const playlist of playlists) {
await loadPlaylist(playlist.url)
log.print(`\nProcessing '${playlist.url}'...`)
await parser
.parsePlaylist(playlist.url)
.then(addToBuffer)
.then(removeDuplicates)
.then(savePlaylist)
.then(done)
.then(p => p.save())
}
if (playlists.length) {
await loadPlaylist('channels/unsorted.m3u')
log.print(`\nProcessing 'channels/unsorted.m3u'...`)
await parser
.parsePlaylist('channels/unsorted.m3u')
.then(removeUnsortedDuplicates)
.then(savePlaylist)
.then(done)
.then(p => p.save())
}
finish()
}
function parseIndex() {
console.info(`Parsing 'index.m3u'...`)
let playlists = parser.parseIndex()
playlists = playlists.filter(i => i.url !== 'channels/unsorted.m3u')
console.info(`Found ${playlists.length} playlist(s)\n`)
return playlists
}
async function loadPlaylist(url) {
console.info(`Processing '${url}'...`)
return parser.parsePlaylist(url)
log.print('\n')
log.finish()
}
async function addToBuffer(playlist) {
if (playlist.url === 'channels/unsorted.m3u') return playlist
globalBuffer = globalBuffer.concat(playlist.channels)
return playlist
}
async function removeDuplicates(playlist) {
console.info(` Looking for duplicates...`)
let buffer = {}
const channels = playlist.channels.filter(i => {
const url = utils.removeProtocol(i.url)
@ -58,13 +48,16 @@ async function removeDuplicates(playlist) {
return result
})
playlist.channels = channels
if (playlist.channels.length !== channels.length) {
log.print('updated')
playlist.channels = channels
playlist.updated = true
}
return playlist
}
async function removeUnsortedDuplicates(playlist) {
console.info(` Looking for duplicates...`)
// locally
let buffer = {}
let channels = playlist.channels.filter(i => {
@ -74,37 +67,18 @@ async function removeUnsortedDuplicates(playlist) {
return result
})
// globally
const urls = globalBuffer.map(i => utils.removeProtocol(i.url))
channels = channels.filter(i => !urls.includes(utils.removeProtocol(i.url)))
if (channels.length === playlist.channels.length) return playlist
playlist.channels = channels
return playlist
}
async function savePlaylist(playlist) {
const original = utils.readFile(playlist.url)
const output = playlist.toString({ raw: true })
if (original === output) {
console.info(`No changes have been made.`)
return false
} else {
utils.createFile(playlist.url, output)
console.info(`Playlist has been updated.`)
if (channels.length !== playlist.channels.length) {
log.print('updated')
playlist.channels = channels
playlist.updated = true
}
return true
}
async function done() {
console.info(` `)
}
function finish() {
console.info('Done.')
return playlist
}
main()

@ -0,0 +1,35 @@
const parser = require('./helpers/parser')
const utils = require('./helpers/utils')
const log = require('./helpers/log')
async function main() {
log.start()
log.print(`Parsing 'index.m3u'...`)
let playlists = parser.parseIndex().filter(i => i.url !== 'channels/unsorted.m3u')
for (const playlist of playlists) {
log.print(`\nProcessing '${playlist.url}'...`)
await parser
.parsePlaylist(playlist.url)
.then(sortChannels)
.then(p => p.save())
}
log.print('\n')
log.finish()
}
async function sortChannels(playlist) {
const channels = [...playlist.channels]
utils.sortBy(channels, ['name', 'url'])
if (JSON.stringify(channels) !== JSON.stringify(playlist.channels)) {
log.print('updated')
playlist.channels = channels
playlist.updated = true
}
return playlist
}
main()

@ -1,20 +1,25 @@
const utils = require('./utils')
const db = require('./db')
const parser = require('./parser')
db.load()
function main() {
start()
const utils = require('./helpers/utils')
const file = require('./helpers/file')
const log = require('./helpers/log')
const db = require('./helpers/db')
async function main() {
log.start()
await loadDatabase()
generateCategoriesTable()
generateCountriesTable()
generateLanguagesTable()
generateReadme()
finish()
log.finish()
}
async function loadDatabase() {
log.print('Loading database...\n')
await db.load()
}
function generateCategoriesTable() {
console.log(`Generating categories table...`)
log.print('Generating categories table...\n')
const categories = []
for (const category of [...db.categories.all(), { name: 'Other', id: 'other' }]) {
@ -25,7 +30,7 @@ function generateCategoriesTable() {
})
}
const table = utils.generateTable(categories, {
const table = generateTable(categories, {
columns: [
{ name: 'Category', align: 'left' },
{ name: 'Channels', align: 'right' },
@ -33,11 +38,11 @@ function generateCategoriesTable() {
]
})
utils.createFile('./.readme/_categories.md', table)
file.create('./.readme/_categories.md', table)
}
function generateCountriesTable() {
console.log(`Generating countries table...`)
log.print('Generating countries table...\n')
const countries = []
for (const country of [
@ -53,7 +58,7 @@ function generateCountriesTable() {
})
}
const table = utils.generateTable(countries, {
const table = generateTable(countries, {
columns: [
{ name: 'Country', align: 'left' },
{ name: 'Channels', align: 'right' },
@ -61,11 +66,11 @@ function generateCountriesTable() {
]
})
utils.createFile('./.readme/_countries.md', table)
file.create('./.readme/_countries.md', table)
}
function generateLanguagesTable() {
console.log(`Generating languages table...`)
log.print('Generating languages table...\n')
const languages = []
for (const language of [
@ -79,7 +84,7 @@ function generateLanguagesTable() {
})
}
const table = utils.generateTable(languages, {
const table = generateTable(languages, {
columns: [
{ name: 'Language', align: 'left' },
{ name: 'Channels', align: 'right' },
@ -87,20 +92,41 @@ function generateLanguagesTable() {
]
})
utils.createFile('./.readme/_languages.md', table)
file.create('./.readme/_languages.md', table)
}
function generateReadme() {
console.log(`Generating README.md...`)
utils.compileMarkdown('../.readme/config.json')
}
function generateTable(data, options) {
let output = '<table>\n'
function start() {
console.log(`Starting...`)
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
}
function finish() {
console.log(`Done.`)
function generateReadme() {
log.print('Generating README.md...\n')
file.compileMarkdown('.readme/config.json')
}
main()

Loading…
Cancel
Save