Merge branch 'master' into patch-2025.01.2

This commit is contained in:
freearhey 2025-01-10 11:57:43 +03:00
commit 979db51d7e
182 changed files with 7599 additions and 8012 deletions

View file

@ -1,39 +0,0 @@
{
"env": {
"node": true,
"es2021": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-case-declarations": "off",
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"semi": [
"error",
"never"
]
}
}

42
.github/workflows/check.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: check
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened, edited]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: tj-actions/changed-files@v45
id: files
with:
since_last_remote_commit: true
files_yaml: |
js:
- tests/**/*.{js,ts}
- scripts/**/*.{js,ts}
- sites/**/*.{js,ts}
channels:
- sites/**/*.channels.xml
- uses: actions/setup-node@v4
if: ${{ !env.ACT && (steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true') }}
with:
node-version: 22
cache: 'npm'
- name: install dependencies
if: steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true'
run: SKIP_POSTINSTALL=1 npm install
- name: check changed js-files
if: steps.files.outputs.js_any_changed == 'true'
run: |
npx eslint ${{ steps.files.outputs.js_all_changed_files }}
- name: check changed *.channels.xml
if: steps.files.outputs.channels_any_changed == 'true'
run: |
npm run channels:lint -- ${{ steps.files.outputs.channels_all_changed_files }}

2
.husky/pre-commit Normal file
View file

@ -0,0 +1,2 @@
npm run lint
npm run channels:lint

View file

@ -1,7 +1,7 @@
module.exports = { module.exports = {
tabWidth: 2, tabWidth: 2,
useTabs: false, useTabs: false,
endOfLine: 'lf', endOfLine: 'crlf',
semi: false, semi: false,
singleQuote: true, singleQuote: true,
printWidth: 100, printWidth: 100,

1
.sites/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_table.md

View file

@ -1,207 +0,0 @@
<table>
<thead>
<tr><th align="left">Site</th><th align="left">Status</th><th align="left">Notes</th></tr>
</thead>
<tbody>
<tr><td><a href="sites/9tv.co.il">9tv.co.il</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/abc.net.au">abc.net.au</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.dk">allente.dk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.fi">allente.fi</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.no">allente.no</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.se">allente.se</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/andorradifusio.ad">andorradifusio.ad</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/anteltv.com.uy">anteltv.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/arianaafgtv.com">arianaafgtv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/arianatelevision.com">arianatelevision.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/arirang.com">arirang.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/artonline.tv">artonline.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/awilime.com">awilime.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/bein.com">bein.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/beinsports.com">beinsports.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/berrymedia.co.kr">berrymedia.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cablego.com.pe">cablego.com.pe</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cableplus.com.uy">cableplus.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/canalplus-haiti.com">canalplus-haiti.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2237</td></tr>
<tr><td><a href="sites/canalplus.com">canalplus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cgates.lt">cgates.lt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/chada.ma">chada.ma</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/chaines-tv.orange.fr">chaines-tv.orange.fr</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2395</td></tr>
<tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/comteco.com.bo">comteco.com.bo</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2239</td></tr>
<tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cosmote.gr">cosmote.gr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/dens.tv">dens.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/digiturk.com.tr">digiturk.com.tr</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2304, https://github.com/iptv-org/epg/issues/2547</td></tr>
<tr><td><a href="sites/directv.com">directv.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2284</td></tr>
<tr><td><a href="sites/directv.com.ar">directv.com.ar</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2339</td></tr>
<tr><td><a href="sites/directv.com.uy">directv.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/dishtv.in">dishtv.in</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2445</td></tr>
<tr><td><a href="sites/dsmart.com.tr">dsmart.com.tr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/dstv.com">dstv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/elcinema.com">elcinema.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2541</td></tr>
<tr><td><a href="sites/ena.skylifetv.co.kr">ena.skylifetv.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/energeek.cl">energeek.cl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/entertainment.ie">entertainment.ie</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/firstmedia.com">firstmedia.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/flixed.io">flixed.io</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/foxsports.com.au">foxsports.com.au</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/foxtel.com.au">foxtel.com.au</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/frikanalen.no">frikanalen.no</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/gatotv.com">gatotv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/getafteritmedia.com">getafteritmedia.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/guida.tv">guida.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/hd-plus.de">hd-plus.de</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2173</td></tr>
<tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2396</td></tr>
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ionplustv.com">ionplustv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ipko.tv">ipko.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
<tr><td><a href="sites/knr.gl">knr.gl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/kplus.vn">kplus.vn</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2240</td></tr>
<tr><td><a href="sites/kvf.fo">kvf.fo</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/m.tv.sms.cz">m.tv.sms.cz</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2241</td></tr>
<tr><td><a href="sites/m.tving.com">m.tving.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/magticom.ge">magticom.ge</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mako.co.il">mako.co.il</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/maxtv.hrvatskitelekom.hr">maxtv.hrvatskitelekom.hr</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2509</td></tr>
<tr><td><a href="sites/maxtvgo.mk">maxtvgo.mk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mediagenie.co.kr">mediagenie.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mediaklikk.hu">mediaklikk.hu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mediasetinfinity.mediaset.it">mediasetinfinity.mediaset.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/melita.com">melita.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/meo.pt">meo.pt</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2446</td></tr>
<tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/moji.id">moji.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mon-programme-tv.be">mon-programme-tv.be</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/movistarplus.es">movistarplus.es</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2498</td></tr>
<tr><td><a href="sites/mtel.ba">mtel.ba</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mts.rs">mts.rs</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mujtvprogram.cz">mujtvprogram.cz</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/musor.tv">musor.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mysky.com.ph">mysky.com.ph</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mytelly.co.uk">mytelly.co.uk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mytvsuper.com">mytvsuper.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/neo.io">neo.io</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nhkworldpremium.com">nhkworldpremium.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nhl.com">nhl.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nostv.pt">nostv.pt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/novacyprus.com">novacyprus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/novasports.gr">novasports.gr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nowplayer.now.com">nowplayer.now.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nuevosiglo.com.uy">nuevosiglo.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nzxmltv.com">nzxmltv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ontvtonight.com">ontvtonight.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/osn.com">osn.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/pbsguam.org">pbsguam.org</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/pickx.be">pickx.be</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/player.ee.co.uk">player.ee.co.uk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/playtv.unifi.com.my">playtv.unifi.com.my</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/plex.tv">plex.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programacion-tv.elpais.com">programacion-tv.elpais.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programacion.tcc.com.uy">programacion.tcc.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programetv.ro">programetv.ro</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programme-tv.net">programme-tv.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programme-tv.vini.pf">programme-tv.vini.pf</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programme.tvb.com">programme.tvb.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programtv.onet.pl">programtv.onet.pl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/raiplay.it">raiplay.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/reportv.com.ar">reportv.com.ar</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rev.bs">rev.bs</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2255</td></tr>
<tr><td><a href="sites/rotana.net">rotana.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rtb.gov.bn">rtb.gov.bn</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2257</td></tr>
<tr><td><a href="sites/rthk.hk">rthk.hk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rtmklik.rtm.gov.my">rtmklik.rtm.gov.my</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rtp.pt">rtp.pt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ruv.is">ruv.is</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/s.mxtv.jp">s.mxtv.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sat.tv">sat.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/shahid.mbc.net">shahid.mbc.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/siba.com.co">siba.com.co</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sky.com">sky.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2516, https://github.com/iptv-org/epg/issues/2501</td></tr>
<tr><td><a href="sites/sky.de">sky.de</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sporttv.pt">sporttv.pt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/starhubtvplus.com">starhubtvplus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/startimestv.com">startimestv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/streamingtvguides.com">streamingtvguides.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/teliatv.ee">teliatv.ee</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv-programme.telecablesat.fr">tv-programme.telecablesat.fr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.lv">tv.lv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.nu">tv.nu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.yettel.hu">tv.yettel.hu</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2263</td></tr>
<tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv24.se">tv24.se</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tva.tv">tva.tv</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2264</td></tr>
<tr><td><a href="sites/tvarenasport.com">tvarenasport.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvarenasport.hr">tvarenasport.hr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvcesoir.fr">tvcesoir.fr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvcubana.icrt.cu">tvcubana.icrt.cu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvgids.nl">tvgids.nl</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2400</td></tr>
<tr><td><a href="sites/tvguide.com">tvguide.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2542</td></tr>
<tr><td><a href="sites/tvguide.myjcom.jp">tvguide.myjcom.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvhebdo.com">tvhebdo.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvheute.at">tvheute.at</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvim.tv">tvim.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvireland.ie">tvireland.ie</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvmi.mt">tvmi.mt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2272</td></tr>
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2399</td></tr>
<tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2176</td></tr>
<tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/vivacom.bg">vivacom.bg</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2270</td></tr>
<tr><td><a href="sites/vtm.be">vtm.be</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/walesi.com.fj">walesi.com.fj</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/watch.sportsnet.ca">watch.sportsnet.ca</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/watchyour.tv">watchyour.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/wavve.com">wavve.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/web.magentatv.de">web.magentatv.de</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/webtv.delta.nl">webtv.delta.nl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/winplay.co">winplay.co</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/worldfishingnetwork.com">worldfishingnetwork.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/www3.nhk.or.jp">www3.nhk.or.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/xumo.tv">xumo.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/zap.co.ao">zap.co.ao</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ziggogo.tv">ziggogo.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/znbc.co.zm">znbc.co.zm</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/zuragt.mn">zuragt.mn</a></td><td>🟢</td><td></td></tr>
</tbody>
</table>

View file

@ -1,4 +1,4 @@
# Sites # Sites
<!-- prettier-ignore --> <!-- prettier-ignore -->
#include "./.sites/_table.md" #include "./.sites/_table.md"

View file

@ -147,6 +147,7 @@ For scripts to work, you must have [Node.js](https://nodejs.org/en) installed on
To run scripts use the `npm run <script-name>` command. To run scripts use the `npm run <script-name>` command.
- `act:check`: allows to test the [check](https://github.com/iptv-org/iptv/blob/master/.github/workflows/check.yml) workflow locally. Depends on [nektos/act](https://github.com/nektos/act).
- `act:update`: allows to test the [update](https://github.com/iptv-org/iptv/blob/master/.github/workflows/update.yml) workflow locally. Depends on [nektos/act](https://github.com/nektos/act). - `act:update`: allows to test the [update](https://github.com/iptv-org/iptv/blob/master/.github/workflows/update.yml) workflow locally. Depends on [nektos/act](https://github.com/nektos/act).
- `api:load`: downloads the latest channels data from the [iptv-org/api](https://github.com/iptv-org/api). - `api:load`: downloads the latest channels data from the [iptv-org/api](https://github.com/iptv-org/api).
- `api:generate`: generates a JSON file with all channels for the [iptv-org/api](https://github.com/iptv-org/api) repository. - `api:generate`: generates a JSON file with all channels for the [iptv-org/api](https://github.com/iptv-org/api) repository.

427
SITES.md
View file

@ -1,212 +1,215 @@
# Sites # Sites
<!-- prettier-ignore --> <!-- prettier-ignore -->
<table> <table>
<thead> <thead>
<tr><th align="left">Site</th><th align="left">Status</th><th align="left">Notes</th></tr> <tr><th align="left">Site</th><th align="left">Status</th><th align="left">Notes</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td><a href="sites/9tv.co.il">9tv.co.il</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/9tv.co.il">9tv.co.il</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/abc.net.au">abc.net.au</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/abc.net.au">abc.net.au</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.dk">allente.dk</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/allente.dk">allente.dk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.fi">allente.fi</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/allente.fi">allente.fi</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.no">allente.no</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/allente.no">allente.no</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/allente.se">allente.se</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/allente.se">allente.se</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/andorradifusio.ad">andorradifusio.ad</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/andorradifusio.ad">andorradifusio.ad</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/anteltv.com.uy">anteltv.com.uy</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/anteltv.com.uy">anteltv.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/arianaafgtv.com">arianaafgtv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/arianaafgtv.com">arianaafgtv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/arianatelevision.com">arianatelevision.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/arianatelevision.com">arianatelevision.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/arirang.com">arirang.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/arirang.com">arirang.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/artonline.tv">artonline.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/artonline.tv">artonline.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/awilime.com">awilime.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/awilime.com">awilime.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/bein.com">bein.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/bein.com">bein.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/beinsports.com">beinsports.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/beinsports.com">beinsports.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/berrymedia.co.kr">berrymedia.co.kr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/berrymedia.co.kr">berrymedia.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cablego.com.pe">cablego.com.pe</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/cablego.com.pe">cablego.com.pe</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cableplus.com.uy">cableplus.com.uy</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/cableplus.com.uy">cableplus.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/canalplus-haiti.com">canalplus-haiti.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2237</td></tr> <tr><td><a href="sites/canalplus-haiti.com">canalplus-haiti.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2237</td></tr>
<tr><td><a href="sites/canalplus.com">canalplus.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/canalplus.com">canalplus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cgates.lt">cgates.lt</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/cgates.lt">cgates.lt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/chada.ma">chada.ma</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/chada.ma">chada.ma</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/chaines-tv.orange.fr">chaines-tv.orange.fr</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2395</td></tr> <tr><td><a href="sites/chaines-tv.orange.fr">chaines-tv.orange.fr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/comteco.com.bo">comteco.com.bo</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2239</td></tr> <tr><td><a href="sites/comteco.com.bo">comteco.com.bo</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2239</td></tr>
<tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cosmotetv.gr">cosmotetv.gr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/cosmotetv.gr">cosmotetv.gr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/dens.tv">dens.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/cyta.com.cy">cyta.com.cy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/digiturk.com.tr">digiturk.com.tr</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2304, https://github.com/iptv-org/epg/issues/2547</td></tr> <tr><td><a href="sites/dens.tv">dens.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/directv.com">directv.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2284</td></tr> <tr><td><a href="sites/digiturk.com.tr">digiturk.com.tr</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2304, https://github.com/iptv-org/epg/issues/2547</td></tr>
<tr><td><a href="sites/directv.com.ar">directv.com.ar</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2339</td></tr> <tr><td><a href="sites/directv.com">directv.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2284</td></tr>
<tr><td><a href="sites/directv.com.uy">directv.com.uy</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/directv.com.ar">directv.com.ar</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2339</td></tr>
<tr><td><a href="sites/dishtv.in">dishtv.in</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2445</td></tr> <tr><td><a href="sites/directv.com.uy">directv.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/dsmart.com.tr">dsmart.com.tr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/dishtv.in">dishtv.in</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2445</td></tr>
<tr><td><a href="sites/dstv.com">dstv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/dsmart.com.tr">dsmart.com.tr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/elcinema.com">elcinema.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2541</td></tr> <tr><td><a href="sites/dstv.com">dstv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ena.skylifetv.co.kr">ena.skylifetv.co.kr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/elcinema.com">elcinema.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/energeek.cl">energeek.cl</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/ena.skylifetv.co.kr">ena.skylifetv.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/entertainment.ie">entertainment.ie</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/energeek.cl">energeek.cl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/firstmedia.com">firstmedia.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/entertainment.ie">entertainment.ie</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/flixed.io">flixed.io</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/firstmedia.com">firstmedia.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/foxsports.com.au">foxsports.com.au</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/flixed.io">flixed.io</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/foxtel.com.au">foxtel.com.au</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/foxsports.com.au">foxsports.com.au</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/frikanalen.no">frikanalen.no</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/foxtel.com.au">foxtel.com.au</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/gatotv.com">gatotv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/frikanalen.no">frikanalen.no</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/getafteritmedia.com">getafteritmedia.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/gatotv.com">gatotv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/guida.tv">guida.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/getafteritmedia.com">getafteritmedia.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/guida.tv">guida.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/hd-plus.de">hd-plus.de</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2173</td></tr> <tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/hd-plus.de">hd-plus.de</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2173</td></tr>
<tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2556</td></tr> <tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2556</td></tr>
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2396</td></tr> <tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2396</td></tr>
<tr><td><a href="sites/ionplustv.com">ionplustv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ipko.tv">ipko.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/ionplustv.com">ionplustv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr> <tr><td><a href="sites/ipko.tv">ipko.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/knr.gl">knr.gl</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
<tr><td><a href="sites/kplus.vn">kplus.vn</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2240</td></tr> <tr><td><a href="sites/knr.gl">knr.gl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/kvf.fo">kvf.fo</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/kplus.vn">kplus.vn</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2240</td></tr>
<tr><td><a href="sites/m.tv.sms.cz">m.tv.sms.cz</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2241</td></tr> <tr><td><a href="sites/kvf.fo">kvf.fo</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/m.tving.com">m.tving.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/m.tv.sms.cz">m.tv.sms.cz</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2241</td></tr>
<tr><td><a href="sites/magticom.ge">magticom.ge</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/m.tving.com">m.tving.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mako.co.il">mako.co.il</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/magticom.ge">magticom.ge</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/maxtv.hrvatskitelekom.hr">maxtv.hrvatskitelekom.hr</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2509</td></tr> <tr><td><a href="sites/mako.co.il">mako.co.il</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/maxtvgo.mk">maxtvgo.mk</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/maxtv.hrvatskitelekom.hr">maxtv.hrvatskitelekom.hr</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2509</td></tr>
<tr><td><a href="sites/mediagenie.co.kr">mediagenie.co.kr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/maxtvgo.mk">maxtvgo.mk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mediaklikk.hu">mediaklikk.hu</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mediagenie.co.kr">mediagenie.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mediasetinfinity.mediaset.it">mediasetinfinity.mediaset.it</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mediaklikk.hu">mediaklikk.hu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/melita.com">melita.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mediasetinfinity.mediaset.it">mediasetinfinity.mediaset.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/meo.pt">meo.pt</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2446</td></tr> <tr><td><a href="sites/melita.com">melita.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/meo.pt">meo.pt</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2446</td></tr>
<tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mi.tv">mi.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/moji.id">moji.id</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mon-programme-tv.be">mon-programme-tv.be</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/moji.id">moji.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/movistarplus.es">movistarplus.es</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2498</td></tr> <tr><td><a href="sites/mon-programme-tv.be">mon-programme-tv.be</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mtel.ba">mtel.ba</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/movistarplus.es">movistarplus.es</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2498</td></tr>
<tr><td><a href="sites/mts.rs">mts.rs</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mtel.ba">mtel.ba</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mujtvprogram.cz">mujtvprogram.cz</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mts.rs">mts.rs</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/musor.tv">musor.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mujtvprogram.cz">mujtvprogram.cz</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mysky.com.ph">mysky.com.ph</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/musor.tv">musor.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mytelly.co.uk">mytelly.co.uk</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mysky.com.ph">mysky.com.ph</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/mytvsuper.com">mytvsuper.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mytelly.co.uk">mytelly.co.uk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/neo.io">neo.io</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/mytvsuper.com">mytvsuper.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nhkworldpremium.com">nhkworldpremium.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/neo.io">neo.io</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nhl.com">nhl.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/nhkworldpremium.com">nhkworldpremium.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nostv.pt">nostv.pt</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/nhl.com">nhl.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/novacyprus.com">novacyprus.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/nostv.pt">nostv.pt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/novasports.gr">novasports.gr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/novacyprus.com">novacyprus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nowplayer.now.com">nowplayer.now.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/novasports.gr">novasports.gr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nuevosiglo.com.uy">nuevosiglo.com.uy</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/nowplayer.now.com">nowplayer.now.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/nzxmltv.com">nzxmltv.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2557</td></tr> <tr><td><a href="sites/nuevosiglo.com.uy">nuevosiglo.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ontvtonight.com">ontvtonight.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/nzxmltv.com">nzxmltv.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2557</td></tr>
<tr><td><a href="sites/orangetv.orange.es">orangetv.orange.es</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/ontvtonight.com">ontvtonight.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/osn.com">osn.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/orangetv.orange.es">orangetv.orange.es</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/pbsguam.org">pbsguam.org</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/osn.com">osn.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/pickx.be">pickx.be</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/pbsguam.org">pbsguam.org</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/player.ee.co.uk">player.ee.co.uk</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/pickx.be">pickx.be</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/playtv.unifi.com.my">playtv.unifi.com.my</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/player.ee.co.uk">player.ee.co.uk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/plex.tv">plex.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/playtv.unifi.com.my">playtv.unifi.com.my</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programacion-tv.elpais.com">programacion-tv.elpais.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/plex.tv">plex.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programacion.tcc.com.uy">programacion.tcc.com.uy</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/pluto.tv">pluto.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programetv.ro">programetv.ro</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programacion-tv.elpais.com">programacion-tv.elpais.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programme-tv.net">programme-tv.net</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programacion.tcc.com.uy">programacion.tcc.com.uy</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programme-tv.vini.pf">programme-tv.vini.pf</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programetv.ro">programetv.ro</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programme.tvb.com">programme.tvb.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programme-tv.net">programme-tv.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/programtv.onet.pl">programtv.onet.pl</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programme-tv.vini.pf">programme-tv.vini.pf</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/raiplay.it">raiplay.it</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programme.tvb.com">programme.tvb.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/reportv.com.ar">reportv.com.ar</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/programtv.onet.pl">programtv.onet.pl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rev.bs">rev.bs</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2255</td></tr> <tr><td><a href="sites/raiplay.it">raiplay.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rotana.net">rotana.net</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/reportv.com.ar">reportv.com.ar</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rtb.gov.bn">rtb.gov.bn</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2257</td></tr> <tr><td><a href="sites/rev.bs">rev.bs</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2255</td></tr>
<tr><td><a href="sites/rthk.hk">rthk.hk</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/rotana.net">rotana.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/rtmklik.rtm.gov.my">rtmklik.rtm.gov.my</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/rtb.gov.bn">rtb.gov.bn</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2257</td></tr>
<tr><td><a href="sites/rtp.pt">rtp.pt</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/rthk.hk">rthk.hk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ruv.is">ruv.is</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/rtmklik.rtm.gov.my">rtmklik.rtm.gov.my</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/s.mxtv.jp">s.mxtv.jp</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/rtp.pt">rtp.pt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sat.tv">sat.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/ruv.is">ruv.is</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/shahid.mbc.net">shahid.mbc.net</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/s.mxtv.jp">s.mxtv.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/siba.com.co">siba.com.co</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/sat.tv">sat.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/shahid.mbc.net">shahid.mbc.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/siba.com.co">siba.com.co</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/singtel.com">singtel.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sky.com">sky.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sky.de">sky.de</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/sky.com">sky.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/sky.de">sky.de</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/sporttv.pt">sporttv.pt</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/starhubtvplus.com">starhubtvplus.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/startimestv.com">startimestv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/sporttv.pt">sporttv.pt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/streamingtvguides.com">streamingtvguides.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/starhubtvplus.com">starhubtvplus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/startimestv.com">startimestv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/stod2.is">stod2.is</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/streamingtvguides.com">streamingtvguides.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/teliatv.ee">teliatv.ee</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tivie.id">tivie.id</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/teliatv.ee">teliatv.ee</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tivie.id">tivie.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv-programme.telecablesat.fr">tv-programme.telecablesat.fr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv-programme.telecablesat.fr">tv-programme.telecablesat.fr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.lv">tv.lv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.lv">tv.lv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.nu">tv.nu</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.nu">tv.nu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv.yettel.hu">tv.yettel.hu</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2263</td></tr> <tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv24.se">tv24.se</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv.yettel.hu">tv.yettel.hu</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2263</td></tr>
<tr><td><a href="sites/tva.tv">tva.tv</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2264</td></tr> <tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvarenasport.com">tvarenasport.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv24.se">tv24.se</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvarenasport.hr">tvarenasport.hr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvcesoir.fr">tvcesoir.fr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tva.tv">tva.tv</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2264</td></tr>
<tr><td><a href="sites/tvcubana.icrt.cu">tvcubana.icrt.cu</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvarenasport.com">tvarenasport.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvgids.nl">tvgids.nl</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2400</td></tr> <tr><td><a href="sites/tvarenasport.hr">tvarenasport.hr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvguide.com">tvguide.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2542</td></tr> <tr><td><a href="sites/tvcesoir.fr">tvcesoir.fr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvguide.myjcom.jp">tvguide.myjcom.jp</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvcubana.icrt.cu">tvcubana.icrt.cu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvhebdo.com">tvhebdo.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvgids.nl">tvgids.nl</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2400</td></tr>
<tr><td><a href="sites/tvheute.at">tvheute.at</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvguide.com">tvguide.com</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2542</td></tr>
<tr><td><a href="sites/tvim.tv">tvim.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvguide.myjcom.jp">tvguide.myjcom.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvireland.ie">tvireland.ie</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvhebdo.com">tvhebdo.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvmi.mt">tvmi.mt</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvheute.at">tvheute.at</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvim.tv">tvim.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2272</td></tr> <tr><td><a href="sites/tvireland.ie">tvireland.ie</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvmi.mt">tvmi.mt</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2399</td></tr> <tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2176</td></tr> <tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2272</td></tr>
<tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2399</td></tr>
<tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2176</td></tr>
<tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/vidio.com">vidio.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/vivacom.bg">vivacom.bg</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2270</td></tr> <tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/vtm.be">vtm.be</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/walesi.com.fj">walesi.com.fj</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/watch.sportsnet.ca">watch.sportsnet.ca</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/vivacom.bg">vivacom.bg</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2270</td></tr>
<tr><td><a href="sites/watchyour.tv">watchyour.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/vtm.be">vtm.be</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/wavve.com">wavve.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/walesi.com.fj">walesi.com.fj</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/web.magentatv.de">web.magentatv.de</a></td><td>🔴</td><td>https://github.com/iptv-org/epg/issues/2555</td></tr> <tr><td><a href="sites/watch.sportsnet.ca">watch.sportsnet.ca</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/webtv.delta.nl">webtv.delta.nl</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/watchyour.tv">watchyour.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/winplay.co">winplay.co</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/wavve.com">wavve.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/worldfishingnetwork.com">worldfishingnetwork.com</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/web.magentatv.de">web.magentatv.de</a></td><td>🟡</td><td>https://github.com/iptv-org/epg/issues/2570</td></tr>
<tr><td><a href="sites/www3.nhk.or.jp">www3.nhk.or.jp</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/webtv.delta.nl">webtv.delta.nl</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/xumo.tv">xumo.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/winplay.co">winplay.co</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/zap.co.ao">zap.co.ao</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/worldfishingnetwork.com">worldfishingnetwork.com</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/ziggogo.tv">ziggogo.tv</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/www3.nhk.or.jp">www3.nhk.or.jp</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/znbc.co.zm">znbc.co.zm</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/xumo.tv">xumo.tv</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/zuragt.mn">zuragt.mn</a></td><td>🟢</td><td></td></tr> <tr><td><a href="sites/zap.co.ao">zap.co.ao</a></td><td>🟢</td><td></td></tr>
</tbody> <tr><td><a href="sites/ziggogo.tv">ziggogo.tv</a></td><td>🟢</td><td></td></tr>
</table> <tr><td><a href="sites/znbc.co.zm">znbc.co.zm</a></td><td>🟢</td><td></td></tr>
<tr><td><a href="sites/zuragt.mn">zuragt.mn</a></td><td>🟢</td><td></td></tr>
</tbody>
</table>

52
eslint.config.mjs Normal file
View file

@ -0,0 +1,52 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import globals from 'globals'
import tsParser from '@typescript-eslint/parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import js from '@eslint/js'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
})
export default [
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'),
{
plugins: {
'@typescript-eslint': typescriptEslint
},
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-case-declarations': 'off',
'linebreak-style': ['error', 'windows'],
quotes: [
'error',
'single',
{
avoidEscape: true
}
],
semi: ['error', 'never']
}
}
]

631
package-lock.json generated
View file

@ -9,11 +9,15 @@
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@alex_neo/jest-expect-message": "^1.0.5", "@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@freearhey/core": "^0.3.1", "@freearhey/core": "^0.3.1",
"@freearhey/search-js": "^0.1.1", "@freearhey/search-js": "^0.1.1",
"@ntlab/sfetch": "^1.0.0", "@ntlab/sfetch": "^1.0.0",
"@octokit/plugin-paginate-rest": "^11.3.6", "@octokit/plugin-paginate-rest": "^11.3.6",
"@octokit/plugin-rest-endpoint-methods": "^13.2.6", "@octokit/plugin-rest-endpoint-methods": "^13.2.6",
"@swc/core": "^1.10.4",
"@swc/jest": "^0.2.37",
"@types/cli-progress": "^3.11.3", "@types/cli-progress": "^3.11.3",
"@types/fs-extra": "^11.0.2", "@types/fs-extra": "^11.0.2",
"@types/inquirer": "^9.0.3", "@types/inquirer": "^9.0.3",
@ -39,9 +43,12 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"glob": "^7.2.0", "glob": "^7.2.0",
"globals": "^15.14.0",
"husky": "^9.1.7",
"iconv-lite": "^0.4.24", "iconv-lite": "^0.4.24",
"inquirer": "^8.2.6", "inquirer": "^8.2.6",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-offline": "^1.0.1",
"langs": "^2.0.0", "langs": "^2.0.0",
"libxmljs2": "^0.35.0", "libxmljs2": "^0.35.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -58,12 +65,12 @@
"run-script-os": "^1.1.6", "run-script-os": "^1.1.6",
"serve": "^14.2.4", "serve": "^14.2.4",
"signale": "^1.4.0", "signale": "^1.4.0",
"skip-postinstall": "^1.0.0",
"srcset": "^4.0.0", "srcset": "^4.0.0",
"table2array": "^0.0.2", "table2array": "^0.0.2",
"tabletojson": "^2.0.7", "tabletojson": "^2.0.7",
"tough-cookie": "^5.0.0", "tough-cookie": "^5.0.0",
"transliteration": "^2.2.0", "transliteration": "^2.2.0",
"ts-jest": "^29.1.1",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"unzipit": "^1.4.0", "unzipit": "^1.4.0",
"wildcard-match": "^5.1.2" "wildcard-match": "^5.1.2"
@ -639,6 +646,14 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.23.3", "version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz",
@ -1508,6 +1523,17 @@
} }
} }
}, },
"node_modules/@jest/create-cache-key-function": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz",
"integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==",
"dependencies": {
"@jest/types": "^29.6.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/environment": { "node_modules/@jest/environment": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
@ -2246,6 +2272,222 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"node_modules/@swc/core": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.4.tgz",
"integrity": "sha512-ut3zfiTLORMxhr6y/GBxkHmzcGuVpwJYX4qyXWuBKkpw/0g0S5iO1/wW7RnLnZbAi8wS/n0atRZoaZlXWBkeJg==",
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.17"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.10.4",
"@swc/core-darwin-x64": "1.10.4",
"@swc/core-linux-arm-gnueabihf": "1.10.4",
"@swc/core-linux-arm64-gnu": "1.10.4",
"@swc/core-linux-arm64-musl": "1.10.4",
"@swc/core-linux-x64-gnu": "1.10.4",
"@swc/core-linux-x64-musl": "1.10.4",
"@swc/core-win32-arm64-msvc": "1.10.4",
"@swc/core-win32-ia32-msvc": "1.10.4",
"@swc/core-win32-x64-msvc": "1.10.4"
},
"peerDependencies": {
"@swc/helpers": "*"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.4.tgz",
"integrity": "sha512-sV/eurLhkjn/197y48bxKP19oqcLydSel42Qsy2zepBltqUx+/zZ8+/IS0Bi7kaWVFxerbW1IPB09uq8Zuvm3g==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.4.tgz",
"integrity": "sha512-gjYNU6vrAUO4+FuovEo9ofnVosTFXkF0VDuo1MKPItz6e2pxc2ale4FGzLw0Nf7JB1sX4a8h06CN16/pLJ8Q2w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.4.tgz",
"integrity": "sha512-zd7fXH5w8s+Sfvn2oO464KDWl+ZX1MJiVmE4Pdk46N3PEaNwE0koTfgx2vQRqRG4vBBobzVvzICC3618WcefOA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.4.tgz",
"integrity": "sha512-+UGfoHDxsMZgFD3tABKLeEZHqLNOkxStu+qCG7atGBhS4Slri6h6zijVvf4yI5X3kbXdvc44XV/hrP/Klnui2A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.4.tgz",
"integrity": "sha512-cDDj2/uYsOH0pgAnDkovLZvKJpFmBMyXkxEG6Q4yw99HbzO6QzZ5HDGWGWVq/6dLgYKlnnmpjZCPPQIu01mXEg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.4.tgz",
"integrity": "sha512-qJXh9D6Kf5xSdGWPINpLGixAbB5JX8JcbEJpRamhlDBoOcQC79dYfOMEIxWPhTS1DGLyFakAx2FX/b2VmQmj0g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.4.tgz",
"integrity": "sha512-A76lIAeyQnHCVt0RL/pG+0er8Qk9+acGJqSZOZm67Ve3B0oqMd871kPtaHBM0BW3OZAhoILgfHW3Op9Q3mx3Cw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.4.tgz",
"integrity": "sha512-e6j5kBu4fIY7fFxFxnZI0MlEovRvp50Lg59Fw+DVbtqHk3C85dckcy5xKP+UoXeuEmFceauQDczUcGs19SRGSQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.4.tgz",
"integrity": "sha512-RSYHfdKgNXV/amY5Tqk1EWVsyQnhlsM//jeqMLw5Fy9rfxP592W9UTumNikNRPdjI8wKKzNMXDb1U29tQjN0dg==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.4.tgz",
"integrity": "sha512-1ujYpaqfqNPYdwKBlvJnOqcl+Syn3UrQ4XE0Txz6zMYgyh6cdU6a3pxqLqIUSJ12MtXRA9ZUhEz1ekU3LfLWXw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/jest": {
"version": "0.2.37",
"resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.37.tgz",
"integrity": "sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==",
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@swc/counter": "^0.1.3",
"jsonc-parser": "^3.2.0"
},
"engines": {
"npm": ">= 7.0.0"
},
"peerDependencies": {
"@swc/core": "*"
}
},
"node_modules/@swc/types": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
"integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@szmarczak/http-timer": { "node_modules/@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -3326,17 +3568,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dependencies": {
"fast-json-stable-stringify": "2.x"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bser": { "node_modules/bser": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -5262,11 +5493,14 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "11.12.0", "version": "15.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
"engines": { "engines": {
"node": ">=4" "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/got": { "node_modules/got": {
@ -5417,6 +5651,20 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -6209,6 +6457,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/jest-offline": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jest-offline/-/jest-offline-1.0.1.tgz",
"integrity": "sha512-pcYJ8rVxWP3SS9de15iSQY87ErLGGgMC4qtVcRLb/qemrefI1IgnAzOusp0eemGu7JoAGlb4oBGnZorehu95KA==",
"dependencies": {
"mitm": "^1.3.2"
}
},
"node_modules/jest-pnp-resolver": { "node_modules/jest-pnp-resolver": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
@ -6562,6 +6818,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="
},
"node_modules/jsonfile": { "node_modules/jsonfile": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@ -6721,11 +6982,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -6796,7 +7052,9 @@
"node_modules/make-error": { "node_modules/make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"optional": true,
"peer": true
}, },
"node_modules/make-fetch-happen": { "node_modules/make-fetch-happen": {
"version": "13.0.1", "version": "13.0.1",
@ -7083,6 +7341,25 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}, },
"node_modules/mitm": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/mitm/-/mitm-1.7.3.tgz",
"integrity": "sha512-linie/mGisDH73C7aiW6JmstA5XskXd15JBJAEeNQBdH3/L0dJdE/yZ+rw/y2zT7Fcib5KAnL5OvxYOOFQbsgw==",
"dependencies": {
"semver": ">= 5 < 6"
},
"engines": {
"node": ">= 0.10.24"
}
},
"node_modules/mitm/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@ -8910,6 +9187,15 @@
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
}, },
"node_modules/skip-postinstall": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/skip-postinstall/-/skip-postinstall-1.0.0.tgz",
"integrity": "sha512-IUVEmm4v7Ubzrp9JDG15oTzMB+abJdHcduXMRzBlHnHRrmpQ/QoPtYCRaorP+abAULTGEh87gPPyyMK5H1X1Dg==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"bin": {
"skip-postinstall": "index.js"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -9406,86 +9692,6 @@
"typescript": ">=4.2.0" "typescript": ">=4.2.0"
} }
}, },
"node_modules/ts-jest": {
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^29.0.0",
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "^7.5.3",
"yargs-parser": "^21.0.1"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@jest/types": "^29.0.0",
"babel-jest": "^29.0.0",
"jest": "^29.0.0",
"typescript": ">=4.3 <6"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@jest/types": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"node_modules/ts-jest/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-jest/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/ts-jest/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/ts-node": { "node_modules/ts-node": {
"version": "10.9.1", "version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@ -10484,6 +10690,13 @@
"@babel/types": "^7.23.3", "@babel/types": "^7.23.3",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0"
},
"dependencies": {
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
}
} }
}, },
"@babel/types": { "@babel/types": {
@ -10994,6 +11207,14 @@
"strip-ansi": "^6.0.0" "strip-ansi": "^6.0.0"
} }
}, },
"@jest/create-cache-key-function": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz",
"integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==",
"requires": {
"@jest/types": "^29.6.3"
}
},
"@jest/environment": { "@jest/environment": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
@ -11591,6 +11812,108 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"@swc/core": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.4.tgz",
"integrity": "sha512-ut3zfiTLORMxhr6y/GBxkHmzcGuVpwJYX4qyXWuBKkpw/0g0S5iO1/wW7RnLnZbAi8wS/n0atRZoaZlXWBkeJg==",
"requires": {
"@swc/core-darwin-arm64": "1.10.4",
"@swc/core-darwin-x64": "1.10.4",
"@swc/core-linux-arm-gnueabihf": "1.10.4",
"@swc/core-linux-arm64-gnu": "1.10.4",
"@swc/core-linux-arm64-musl": "1.10.4",
"@swc/core-linux-x64-gnu": "1.10.4",
"@swc/core-linux-x64-musl": "1.10.4",
"@swc/core-win32-arm64-msvc": "1.10.4",
"@swc/core-win32-ia32-msvc": "1.10.4",
"@swc/core-win32-x64-msvc": "1.10.4",
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.17"
}
},
"@swc/core-darwin-arm64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.4.tgz",
"integrity": "sha512-sV/eurLhkjn/197y48bxKP19oqcLydSel42Qsy2zepBltqUx+/zZ8+/IS0Bi7kaWVFxerbW1IPB09uq8Zuvm3g==",
"optional": true
},
"@swc/core-darwin-x64": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.4.tgz",
"integrity": "sha512-gjYNU6vrAUO4+FuovEo9ofnVosTFXkF0VDuo1MKPItz6e2pxc2ale4FGzLw0Nf7JB1sX4a8h06CN16/pLJ8Q2w==",
"optional": true
},
"@swc/core-linux-arm-gnueabihf": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.4.tgz",
"integrity": "sha512-zd7fXH5w8s+Sfvn2oO464KDWl+ZX1MJiVmE4Pdk46N3PEaNwE0koTfgx2vQRqRG4vBBobzVvzICC3618WcefOA==",
"optional": true
},
"@swc/core-linux-arm64-gnu": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.4.tgz",
"integrity": "sha512-+UGfoHDxsMZgFD3tABKLeEZHqLNOkxStu+qCG7atGBhS4Slri6h6zijVvf4yI5X3kbXdvc44XV/hrP/Klnui2A==",
"optional": true
},
"@swc/core-linux-arm64-musl": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.4.tgz",
"integrity": "sha512-cDDj2/uYsOH0pgAnDkovLZvKJpFmBMyXkxEG6Q4yw99HbzO6QzZ5HDGWGWVq/6dLgYKlnnmpjZCPPQIu01mXEg==",
"optional": true
},
"@swc/core-linux-x64-gnu": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.4.tgz",
"integrity": "sha512-qJXh9D6Kf5xSdGWPINpLGixAbB5JX8JcbEJpRamhlDBoOcQC79dYfOMEIxWPhTS1DGLyFakAx2FX/b2VmQmj0g==",
"optional": true
},
"@swc/core-linux-x64-musl": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.4.tgz",
"integrity": "sha512-A76lIAeyQnHCVt0RL/pG+0er8Qk9+acGJqSZOZm67Ve3B0oqMd871kPtaHBM0BW3OZAhoILgfHW3Op9Q3mx3Cw==",
"optional": true
},
"@swc/core-win32-arm64-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.4.tgz",
"integrity": "sha512-e6j5kBu4fIY7fFxFxnZI0MlEovRvp50Lg59Fw+DVbtqHk3C85dckcy5xKP+UoXeuEmFceauQDczUcGs19SRGSQ==",
"optional": true
},
"@swc/core-win32-ia32-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.4.tgz",
"integrity": "sha512-RSYHfdKgNXV/amY5Tqk1EWVsyQnhlsM//jeqMLw5Fy9rfxP592W9UTumNikNRPdjI8wKKzNMXDb1U29tQjN0dg==",
"optional": true
},
"@swc/core-win32-x64-msvc": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.4.tgz",
"integrity": "sha512-1ujYpaqfqNPYdwKBlvJnOqcl+Syn3UrQ4XE0Txz6zMYgyh6cdU6a3pxqLqIUSJ12MtXRA9ZUhEz1ekU3LfLWXw==",
"optional": true
},
"@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"@swc/jest": {
"version": "0.2.37",
"resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.37.tgz",
"integrity": "sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==",
"requires": {
"@jest/create-cache-key-function": "^29.7.0",
"@swc/counter": "^0.1.3",
"jsonc-parser": "^3.2.0"
}
},
"@swc/types": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
"integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
"requires": {
"@swc/counter": "^0.1.3"
}
},
"@szmarczak/http-timer": { "@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -12372,14 +12695,6 @@
"update-browserslist-db": "^1.0.11" "update-browserslist-db": "^1.0.11"
} }
}, },
"bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"requires": {
"fast-json-stable-stringify": "2.x"
}
},
"bser": { "bser": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -13782,9 +14097,9 @@
} }
}, },
"globals": { "globals": {
"version": "11.12.0", "version": "15.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="
}, },
"got": { "got": {
"version": "11.8.5", "version": "11.8.5",
@ -13888,6 +14203,11 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
}, },
"husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -14438,6 +14758,14 @@
"jest-util": "^29.7.0" "jest-util": "^29.7.0"
} }
}, },
"jest-offline": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jest-offline/-/jest-offline-1.0.1.tgz",
"integrity": "sha512-pcYJ8rVxWP3SS9de15iSQY87ErLGGgMC4qtVcRLb/qemrefI1IgnAzOusp0eemGu7JoAGlb4oBGnZorehu95KA==",
"requires": {
"mitm": "^1.3.2"
}
},
"jest-pnp-resolver": { "jest-pnp-resolver": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
@ -14721,6 +15049,11 @@
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
}, },
"jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="
},
"jsonfile": { "jsonfile": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@ -14849,11 +15182,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"lodash.merge": { "lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -14908,7 +15236,9 @@
"make-error": { "make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"optional": true,
"peer": true
}, },
"make-fetch-happen": { "make-fetch-happen": {
"version": "13.0.1", "version": "13.0.1",
@ -15138,6 +15468,21 @@
} }
} }
}, },
"mitm": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/mitm/-/mitm-1.7.3.tgz",
"integrity": "sha512-linie/mGisDH73C7aiW6JmstA5XskXd15JBJAEeNQBdH3/L0dJdE/yZ+rw/y2zT7Fcib5KAnL5OvxYOOFQbsgw==",
"requires": {
"semver": ">= 5 < 6"
},
"dependencies": {
"semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
}
}
},
"mkdirp": { "mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@ -16465,6 +16810,11 @@
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
}, },
"skip-postinstall": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/skip-postinstall/-/skip-postinstall-1.0.0.tgz",
"integrity": "sha512-IUVEmm4v7Ubzrp9JDG15oTzMB+abJdHcduXMRzBlHnHRrmpQ/QoPtYCRaorP+abAULTGEh87gPPyyMK5H1X1Dg=="
},
"slash": { "slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -16818,49 +17168,6 @@
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
"requires": {} "requires": {}
}, },
"ts-jest": {
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
"integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==",
"requires": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^29.0.0",
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "^7.5.3",
"yargs-parser": "^21.0.1"
},
"dependencies": {
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
}
}
},
"ts-node": { "ts-node": {
"version": "10.9.1", "version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",

View file

@ -1,6 +1,7 @@
{ {
"name": "epg", "name": "epg",
"scripts": { "scripts": {
"act:check": "act pull_request -W .github/workflows/check.yml",
"act:update": "act workflow_dispatch -W .github/workflows/update.yml", "act:update": "act workflow_dispatch -W .github/workflows/update.yml",
"api:load": "npx tsx scripts/commands/api/load.ts", "api:load": "npx tsx scripts/commands/api/load.ts",
"api:generate": "npx tsx scripts/commands/api/generate.ts", "api:generate": "npx tsx scripts/commands/api/generate.ts",
@ -10,29 +11,37 @@
"channels:validate": "npx tsx scripts/commands/channels/validate.ts", "channels:validate": "npx tsx scripts/commands/channels/validate.ts",
"sites:update": "npx tsx scripts/commands/sites/update.ts", "sites:update": "npx tsx scripts/commands/sites/update.ts",
"grab": "npx tsx scripts/commands/epg/grab.ts", "grab": "npx tsx scripts/commands/epg/grab.ts",
"lint": "npx eslint \"{scripts,tests}/**/*.{ts,js}\"", "lint": "npx eslint \"{scripts,tests,sites}/**/*.{ts,js}\"",
"test": "run-script-os", "test": "run-script-os",
"test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand", "test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand",
"test:default": "TZ=Pacific/Nauru npx jest --runInBand", "test:default": "TZ=Pacific/Nauru npx jest --runInBand",
"postinstall": "npm run api:load" "postinstall": "skip-postinstall || npm run api:load",
"prepare": "husky"
}, },
"private": true, "private": true,
"author": "Arhey", "author": "Arhey",
"license": "UNLICENSED", "license": "UNLICENSED",
"jest": { "jest": {
"setupFiles": [
"<rootDir>/node_modules/jest-offline"
],
"transform": { "transform": {
"^.+\\.(ts|js)$": "ts-jest" "^.+\\.(ts|js)$": "@swc/jest"
}, },
"testRegex": "(tests|sites)/(.*?/)?.*test.(js|ts)$", "testRegex": "(tests|sites)/(.*?/)?.*test.(js|ts)$",
"testTimeout": 10000 "testTimeout": 10000
}, },
"dependencies": { "dependencies": {
"@alex_neo/jest-expect-message": "^1.0.5", "@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@freearhey/core": "^0.3.1", "@freearhey/core": "^0.3.1",
"@freearhey/search-js": "^0.1.1", "@freearhey/search-js": "^0.1.1",
"@ntlab/sfetch": "^1.0.0", "@ntlab/sfetch": "^1.0.0",
"@octokit/plugin-paginate-rest": "^11.3.6", "@octokit/plugin-paginate-rest": "^11.3.6",
"@octokit/plugin-rest-endpoint-methods": "^13.2.6", "@octokit/plugin-rest-endpoint-methods": "^13.2.6",
"@swc/core": "^1.10.4",
"@swc/jest": "^0.2.37",
"@types/cli-progress": "^3.11.3", "@types/cli-progress": "^3.11.3",
"@types/fs-extra": "^11.0.2", "@types/fs-extra": "^11.0.2",
"@types/inquirer": "^9.0.3", "@types/inquirer": "^9.0.3",
@ -58,9 +67,12 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"glob": "^7.2.0", "glob": "^7.2.0",
"globals": "^15.14.0",
"husky": "^9.1.7",
"iconv-lite": "^0.4.24", "iconv-lite": "^0.4.24",
"inquirer": "^8.2.6", "inquirer": "^8.2.6",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-offline": "^1.0.1",
"langs": "^2.0.0", "langs": "^2.0.0",
"libxmljs2": "^0.35.0", "libxmljs2": "^0.35.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -77,12 +89,12 @@
"run-script-os": "^1.1.6", "run-script-os": "^1.1.6",
"serve": "^14.2.4", "serve": "^14.2.4",
"signale": "^1.4.0", "signale": "^1.4.0",
"skip-postinstall": "^1.0.0",
"srcset": "^4.0.0", "srcset": "^4.0.0",
"table2array": "^0.0.2", "table2array": "^0.0.2",
"tabletojson": "^2.0.7", "tabletojson": "^2.0.7",
"tough-cookie": "^5.0.0", "tough-cookie": "^5.0.0",
"transliteration": "^2.2.0", "transliteration": "^2.2.0",
"ts-jest": "^29.1.1",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"unzipit": "^1.4.0", "unzipit": "^1.4.0",
"wildcard-match": "^5.1.2" "wildcard-match": "^5.1.2"

View file

@ -1,51 +1,51 @@
import { Logger, Storage, Collection } from '@freearhey/core' import { Logger, Storage, Collection } from '@freearhey/core'
import { ChannelsParser } from '../../core' import { ChannelsParser } from '../../core'
import path from 'path' import path from 'path'
import { SITES_DIR, API_DIR } from '../../constants' import { SITES_DIR, API_DIR } from '../../constants'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
type OutputItem = { type OutputItem = {
channel: string | null channel: string | null
site: string site: string
site_id: string site_id: string
site_name: string site_name: string
lang: string lang: string
} }
async function main() { async function main() {
const logger = new Logger() const logger = new Logger()
logger.start('staring...') logger.start('staring...')
logger.info('loading channels...') logger.info('loading channels...')
const sitesStorage = new Storage(SITES_DIR) const sitesStorage = new Storage(SITES_DIR)
const parser = new ChannelsParser({ storage: sitesStorage }) const parser = new ChannelsParser({ storage: sitesStorage })
let files: string[] = [] let files: string[] = []
files = await sitesStorage.list('**/*.channels.xml') files = await sitesStorage.list('**/*.channels.xml')
let parsedChannels = new Collection() let parsedChannels = new Collection()
for (const filepath of files) { for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath)) parsedChannels = parsedChannels.concat(await parser.parse(filepath))
} }
logger.info(` found ${parsedChannels.count()} channel(s)`) logger.info(` found ${parsedChannels.count()} channel(s)`)
const output = parsedChannels.map((channel: Channel): OutputItem => { const output = parsedChannels.map((channel: Channel): OutputItem => {
return { return {
channel: channel.xmltv_id || null, channel: channel.xmltv_id || null,
site: channel.site || '', site: channel.site || '',
site_id: channel.site_id || '', site_id: channel.site_id || '',
site_name: channel.name, site_name: channel.name,
lang: channel.lang || '' lang: channel.lang || ''
} }
}) })
const apiStorage = new Storage(API_DIR) const apiStorage = new Storage(API_DIR)
const outputFilename = 'guides.json' const outputFilename = 'guides.json'
await apiStorage.save('guides.json', output.toJSON()) await apiStorage.save('guides.json', output.toJSON())
logger.info(`saved to "${path.join(API_DIR, outputFilename)}"`) logger.info(`saved to "${path.join(API_DIR, outputFilename)}"`)
} }
main() main()

View file

@ -43,7 +43,7 @@ async function main() {
const channelsIndex = sj.createIndex(channelsContent) const channelsIndex = sj.createIndex(channelsContent)
const buffer = new Dictionary() const buffer = new Dictionary()
for (let option of options.all()) { for (const option of options.all()) {
const channel: Channel = option.channel const channel: Channel = option.channel
if (channel.xmltv_id) { if (channel.xmltv_id) {
if (channel.xmltv_id !== '-') { if (channel.xmltv_id !== '-') {
@ -150,7 +150,7 @@ function getOptions(channelsIndex, channel: Channel) {
const query = channel.name const query = channel.name
.replace(/\s(SD|TV|HD|SD\/HD|HDTV)$/i, '') .replace(/\s(SD|TV|HD|SD\/HD|HDTV)$/i, '')
.replace(/(\(|\)|,)/gi, '') .replace(/(\(|\)|,)/gi, '')
.replace(/\-/gi, ' ') .replace(/-/gi, ' ')
.replace(/\+/gi, '') .replace(/\+/gi, '')
const similar = channelsIndex.search(query).map(item => new ApiChannel(item)) const similar = channelsIndex.search(query).map(item => new ApiChannel(item))

View file

@ -1,7 +1,7 @@
import chalk from 'chalk' import chalk from 'chalk'
import libxml, { ValidationError } from 'libxmljs2' import libxml, { ValidationError } from 'libxmljs2'
import { program } from 'commander' import { program } from 'commander'
import { Logger, Storage, File } from '@freearhey/core' import { Storage, File } from '@freearhey/core'
const xsd = `<?xml version="1.0" encoding="UTF-8"?> const xsd = `<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
@ -23,26 +23,14 @@ const xsd = `<?xml version="1.0" encoding="UTF-8"?>
</xs:element> </xs:element>
</xs:schema>` </xs:schema>`
program program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv)
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
const options = program.opts()
async function main() { async function main() {
const logger = new Logger()
const storage = new Storage() const storage = new Storage()
logger.info('options:')
logger.tree(options)
let errors: ValidationError[] = [] let errors: ValidationError[] = []
const files: string[] = await storage.list(options.channels) const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
for (const filepath of files) { for (const filepath of files) {
const file = new File(filepath) const file = new File(filepath)
if (file.extension() !== 'xml') continue if (file.extension() !== 'xml') continue
@ -51,11 +39,15 @@ async function main() {
let localErrors: ValidationError[] = [] let localErrors: ValidationError[] = []
const xsdDoc = libxml.parseXml(xsd) try {
const doc = libxml.parseXml(xml) const xsdDoc = libxml.parseXml(xsd)
const doc = libxml.parseXml(xml)
if (!doc.validate(xsdDoc)) { if (!doc.validate(xsdDoc)) {
localErrors = doc.validationErrors localErrors = doc.validationErrors
}
} catch (error) {
localErrors.push(error)
} }
if (localErrors.length) { if (localErrors.length) {

View file

@ -2,7 +2,7 @@ import { Logger, File, Collection, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core' import { ChannelsParser, XML } from '../../core'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
import { Command } from 'commander' import { Command } from 'commander'
import path from 'path' import { pathToFileURL } from 'node:url'
const program = new Command() const program = new Command()
program program
@ -26,7 +26,7 @@ async function main() {
const logger = new Logger() const logger = new Logger()
const file = new File(options.config) const file = new File(options.config)
const dir = file.dirname() const dir = file.dirname()
const config = require(path.resolve(options.config)) const config = (await import(pathToFileURL(options.config))).default
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channels = new Collection() let channels = new Collection()

View file

@ -47,7 +47,6 @@ async function main() {
const parsedChannels = await parser.parse(filepath) const parsedChannels = await parser.parse(filepath)
const bufferById = new Dictionary()
const bufferBySiteId = new Dictionary() const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = [] const errors: ValidationError[] = []
parsedChannels.forEach((channel: Channel) => { parsedChannels.forEach((channel: Channel) => {

View file

@ -1,58 +1,58 @@
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core' import { Logger, Storage, Collection } from '@freearhey/core'
import { IssueLoader, HTMLTable, Markdown } from '../../core' import { IssueLoader, HTMLTable, Markdown } from '../../core'
import { Issue, Site } from '../../models' import { Issue, Site } from '../../models'
import { SITES_DIR, DOT_SITES_DIR } from '../../constants' import { SITES_DIR, DOT_SITES_DIR } from '../../constants'
import path from 'path' import path from 'path'
async function main() { async function main() {
const logger = new Logger({ disabled: true }) const logger = new Logger({ disabled: true })
const loader = new IssueLoader() const loader = new IssueLoader()
const storage = new Storage(SITES_DIR) const storage = new Storage(SITES_DIR)
const sites = new Collection() const sites = new Collection()
logger.info('loading list of sites') logger.info('loading list of sites')
const folders = await storage.list('*/') const folders = await storage.list('*/')
logger.info('loading issues...') logger.info('loading issues...')
const issues = await loadIssues(loader) const issues = await loadIssues(loader)
logger.info('putting the data together...') logger.info('putting the data together...')
folders.forEach((domain: string) => { folders.forEach((domain: string) => {
const filteredIssues = issues.filter((issue: Issue) => domain === issue.data.get('site')) const filteredIssues = issues.filter((issue: Issue) => domain === issue.data.get('site'))
const site = new Site({ const site = new Site({
domain, domain,
issues: filteredIssues issues: filteredIssues
}) })
sites.add(site) sites.add(site)
}) })
logger.info('creating sites table...') logger.info('creating sites table...')
let data = new Collection() const data = new Collection()
sites.forEach((site: Site) => { sites.forEach((site: Site) => {
data.add([ data.add([
`<a href="sites/${site.domain}">${site.domain}</a>`, `<a href="sites/${site.domain}">${site.domain}</a>`,
site.getStatus().emoji, site.getStatus().emoji,
site.getIssues().all().join(', ') site.getIssues().all().join(', ')
]) ])
}) })
const table = new HTMLTable(data.all(), [{ name: 'Site' }, { name: 'Status' }, { name: 'Notes' }]) const table = new HTMLTable(data.all(), [{ name: 'Site' }, { name: 'Status' }, { name: 'Notes' }])
const readmeStorage = new Storage(DOT_SITES_DIR) const readmeStorage = new Storage(DOT_SITES_DIR)
await readmeStorage.save('_table.md', table.toString()) await readmeStorage.save('_table.md', table.toString())
logger.info('updating sites.md...') logger.info('updating sites.md...')
const configPath = path.join(DOT_SITES_DIR, 'config.json') const configPath = path.join(DOT_SITES_DIR, 'config.json')
const sitesMarkdown = new Markdown(configPath) const sitesMarkdown = new Markdown(configPath)
sitesMarkdown.compile() sitesMarkdown.compile()
} }
main() main()
async function loadIssues(loader: IssueLoader) { async function loadIssues(loader: IssueLoader) {
const issuesWithStatusWarning = await loader.load({ labels: ['broken guide', 'status:warning'] }) const issuesWithStatusWarning = await loader.load({ labels: ['broken guide', 'status:warning'] })
const issuesWithStatusDown = await loader.load({ labels: ['broken guide', 'status:down'] }) const issuesWithStatusDown = await loader.load({ labels: ['broken guide', 'status:down'] })
return issuesWithStatusWarning.concat(issuesWithStatusDown) return issuesWithStatusWarning.concat(issuesWithStatusDown)
} }

View file

@ -1,5 +1,6 @@
const dayjs = require('dayjs') import dayjs from 'dayjs'
const utc = require('dayjs/plugin/utc') import utc from 'dayjs/plugin/utc'
dayjs.extend(utc) dayjs.extend(utc)
const date = {} const date = {}
@ -10,4 +11,4 @@ date.getUTC = function (d = null) {
return dayjs.utc().startOf('d') return dayjs.utc().startOf('d')
} }
module.exports = date export default date

View file

@ -1,46 +1,46 @@
type Column = { type Column = {
name: string name: string
nowrap?: boolean nowrap?: boolean
align?: string align?: string
} }
type DataItem = string[] type DataItem = string[]
export class HTMLTable { export class HTMLTable {
data: DataItem[] data: DataItem[]
columns: Column[] columns: Column[]
constructor(data: DataItem[], columns: Column[]) { constructor(data: DataItem[], columns: Column[]) {
this.data = data this.data = data
this.columns = columns this.columns = columns
} }
toString() { toString() {
let output = '<table>\n' let output = '<table>\r\n'
output += ' <thead>\n <tr>' output += ' <thead>\r\n <tr>'
for (const column of this.columns) { for (const column of this.columns) {
output += `<th align="left">${column.name}</th>` output += `<th align="left">${column.name}</th>`
} }
output += '</tr>\n </thead>\n' output += '</tr>\r\n </thead>\r\n'
output += ' <tbody>\n' output += ' <tbody>\r\n'
for (const item of this.data) { for (const item of this.data) {
output += ' <tr>' output += ' <tr>'
let i = 0 let i = 0
for (const prop in item) { for (const prop in item) {
const column = this.columns[i] const column = this.columns[i]
const nowrap = column.nowrap ? ' nowrap' : '' const nowrap = column.nowrap ? ' nowrap' : ''
const align = column.align ? ` align="${column.align}"` : '' const align = column.align ? ` align="${column.align}"` : ''
output += `<td${align}${nowrap}>${item[prop]}</td>` output += `<td${align}${nowrap}>${item[prop]}</td>`
i++ i++
} }
output += '</tr>\n' output += '</tr>\r\n'
} }
output += ' </tbody>\n' output += ' </tbody>\r\n'
output += '</table>' output += '</table>'
return output return output
} }
} }

View file

@ -1,40 +1,41 @@
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods' import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
import { paginateRest } from '@octokit/plugin-paginate-rest' import { paginateRest } from '@octokit/plugin-paginate-rest'
import { Octokit } from '@octokit/core' import { Octokit } from '@octokit/core'
import { IssueParser } from './' import { IssueParser } from './'
import { TESTING, OWNER, REPO } from '../constants' import { TESTING, OWNER, REPO } from '../constants'
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods) const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
const octokit = new CustomOctokit() const octokit = new CustomOctokit()
export class IssueLoader { export class IssueLoader {
async load({ labels }: { labels: string[] | string }) { async load({ labels }: { labels: string[] | string }) {
labels = Array.isArray(labels) ? labels.join(',') : labels labels = Array.isArray(labels) ? labels.join(',') : labels
let issues: object[] = [] let issues: object[] = []
if (TESTING) { if (TESTING) {
switch (labels) { switch (labels) {
case 'broken guide,status:warning': case 'broken guide,status:warning':
issues = require('../../tests/__data__/input/issues/broken_guide_warning.js') issues = (await import('../../tests/__data__/input/issues/broken_guide_warning.mjs'))
break .default
case 'broken guide,status:down': break
issues = require('../../tests/__data__/input/issues/broken_guide_down.js') case 'broken guide,status:down':
break issues = (await import('../../tests/__data__/input/issues/broken_guide_down.mjs')).default
} break
} else { }
issues = await octokit.paginate(octokit.rest.issues.listForRepo, { } else {
owner: OWNER, issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
repo: REPO, owner: OWNER,
per_page: 100, repo: REPO,
labels, per_page: 100,
headers: { labels,
'X-GitHub-Api-Version': '2022-11-28' headers: {
} 'X-GitHub-Api-Version': '2022-11-28'
}) }
} })
}
const parser = new IssueParser()
const parser = new IssueParser()
return new Collection(issues).map(parser.parse)
} return new Collection(issues).map(parser.parse)
} }
}

View file

@ -1,34 +1,34 @@
import { Dictionary } from '@freearhey/core' import { Dictionary } from '@freearhey/core'
import { Issue } from '../models' import { Issue } from '../models'
const FIELDS = new Dictionary({ const FIELDS = new Dictionary({
Site: 'site' Site: 'site'
}) })
export class IssueParser { export class IssueParser {
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue { parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
const fields = issue.body.split('###') const fields = issue.body.split('###')
const data = new Dictionary() const data = new Dictionary()
fields.forEach((field: string) => { fields.forEach((field: string) => {
let parsed = field.split(/\r?\n/).filter(Boolean) const parsed = field.split(/\r?\n/).filter(Boolean)
let _label = parsed.shift() let _label = parsed.shift()
_label = _label ? _label.trim() : '' _label = _label ? _label.trim() : ''
let _value = parsed.join('\r\n') let _value = parsed.join('\r\n')
_value = _value ? _value.trim() : '' _value = _value ? _value.trim() : ''
if (!_label || !_value) return data if (!_label || !_value) return data
const id: string = FIELDS.get(_label) const id: string = FIELDS.get(_label)
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
if (!id) return if (!id) return
data.set(id, value) data.set(id, value)
}) })
const labels = issue.labels.map(label => label.name) const labels = issue.labels.map(label => label.name)
return new Issue({ number: issue.number, labels, data }) return new Issue({ number: issue.number, labels, data })
} }
} }

View file

@ -1,13 +1,13 @@
import markdownInclude from 'markdown-include' import markdownInclude from 'markdown-include'
export class Markdown { export class Markdown {
filepath: string filepath: string
constructor(filepath: string) { constructor(filepath: string) {
this.filepath = filepath this.filepath = filepath
} }
compile() { compile() {
markdownInclude.compileFiles(this.filepath) markdownInclude.compileFiles(this.filepath)
} }
} }

View file

@ -1,7 +1,7 @@
import { Storage, Collection, DateTime, Logger } from '@freearhey/core' import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './' import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './'
import { SITES_DIR, DATA_DIR } from '../constants' import { SITES_DIR, DATA_DIR } from '../constants'
import { Channel, SiteConfig } from 'epg-grabber' import { SiteConfig } from 'epg-grabber'
import path from 'path' import path from 'path'
import { GrabOptions } from '../commands/epg/grab' import { GrabOptions } from '../commands/epg/grab'

View file

@ -1,2 +1,2 @@
export * from './issue' export * from './issue'
export * from './site' export * from './site'

View file

@ -1,24 +1,24 @@
import { Dictionary } from '@freearhey/core' import { Dictionary } from '@freearhey/core'
import { OWNER, REPO } from '../constants' import { OWNER, REPO } from '../constants'
type IssueProps = { type IssueProps = {
number: number number: number
labels: string[] labels: string[]
data: Dictionary data: Dictionary
} }
export class Issue { export class Issue {
number: number number: number
labels: string[] labels: string[]
data: Dictionary data: Dictionary
constructor({ number, labels, data }: IssueProps) { constructor({ number, labels, data }: IssueProps) {
this.number = number this.number = number
this.labels = labels this.labels = labels
this.data = data this.data = data
} }
getURL() { getURL() {
return `https://github.com/${OWNER}/${REPO}/issues/${this.number}` return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
} }
} }

View file

@ -1,57 +1,57 @@
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
import { Issue } from './' import { Issue } from './'
enum StatusCode { enum StatusCode {
DOWN = 'down', DOWN = 'down',
WARNING = 'warning', WARNING = 'warning',
OK = 'ok' OK = 'ok'
} }
type Status = { type Status = {
code: StatusCode code: StatusCode
emoji: string emoji: string
} }
type SiteProps = { type SiteProps = {
domain: string domain: string
issues: Collection issues: Collection
} }
export class Site { export class Site {
domain: string domain: string
issues: Collection issues: Collection
constructor({ domain, issues }: SiteProps) { constructor({ domain, issues }: SiteProps) {
this.domain = domain this.domain = domain
this.issues = issues this.issues = issues
} }
getStatus(): Status { getStatus(): Status {
const issuesWithStatusDown = this.issues.filter((issue: Issue) => const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
issue.labels.find(label => label === 'status:down') issue.labels.find(label => label === 'status:down')
) )
if (issuesWithStatusDown.notEmpty()) if (issuesWithStatusDown.notEmpty())
return { return {
code: StatusCode.DOWN, code: StatusCode.DOWN,
emoji: '🔴' emoji: '🔴'
} }
const issuesWithStatusWarning = this.issues.filter((issue: Issue) => const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
issue.labels.find(label => label === 'status:warning') issue.labels.find(label => label === 'status:warning')
) )
if (issuesWithStatusWarning.notEmpty()) if (issuesWithStatusWarning.notEmpty())
return { return {
code: StatusCode.WARNING, code: StatusCode.WARNING,
emoji: '🟡' emoji: '🟡'
} }
return { return {
code: StatusCode.OK, code: StatusCode.OK,
emoji: '🟢' emoji: '🟢'
} }
} }
getIssues(): Collection { getIssues(): Collection {
return this.issues.map((issue: Issue) => issue.getURL()) return this.issues.map((issue: Issue) => issue.getURL())
} }
} }

View file

@ -92,7 +92,7 @@ function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const channelData = data.schedule.find(i => i.channel == channelId) const channelData = data.schedule.find(i => i.channel == channelId)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -1,4 +1,4 @@
const { parser, url, request } = require('./awilime.com.config.js') const { parser, url } = require('./awilime.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')

View file

@ -1,5 +1,4 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
@ -62,7 +61,7 @@ function parseItems(content) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
return [] return []
} }

View file

@ -1,6 +1,4 @@
const { parser, url } = require('./beinsports.com.config.js') const { parser, url } = require('./beinsports.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')

View file

@ -1,54 +1,55 @@
const axios = require('axios'); const cheerio = require('cheerio')
const cheerio = require('cheerio'); const dayjs = require('dayjs')
const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc')
const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone')
const timezone = require('dayjs/plugin/timezone'); const customParseFormat = require('dayjs/plugin/customParseFormat')
const customParseFormat = require('dayjs/plugin/customParseFormat');
dayjs.extend(utc)
dayjs.extend(utc); dayjs.extend(timezone)
dayjs.extend(timezone); dayjs.extend(customParseFormat)
dayjs.extend(customParseFormat);
module.exports = {
module.exports = { site: 'chada.ma',
site: 'chada.ma', channels: 'chada.ma.channels.xml',
channels: 'chada.ma.channels.xml', days: 1,
days: 1, request: {
request: { cache: {
cache: { ttl: 60 * 60 * 1000 // 1 hour
ttl: 60 * 60 * 1000 // 1 hour }
} },
}, url() {
url() { return 'https://chada.ma/fr/chada-tv/grille-tv/'
return 'https://chada.ma/fr/chada-tv/grille-tv/'; },
}, parser: function ({ content }) {
parser: function ({ content }) { const $ = cheerio.load(content)
const $ = cheerio.load(content); const programs = []
const programs = [];
$('#stopfix .posts-area h2').each((i, element) => {
$('#stopfix .posts-area h2').each((i, element) => { const timeRange = $(element).text().trim()
const timeRange = $(element).text().trim(); const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim()))
const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim()));
const titleElement = $(element).next('div').next('h3')
const titleElement = $(element).next('div').next('h3'); const title = titleElement.text().trim()
const title = titleElement.text().trim();
const description = titleElement.next('div').text().trim() || 'No description available'
const description = titleElement.next('div').text().trim() || 'No description available';
programs.push({
programs.push({ title,
title, description,
description, start,
start, stop
stop })
}); })
});
return programs
return programs; }
} }
};
function parseProgramTime(timeStr) {
function parseProgramTime(timeStr) { const timeZone = 'Africa/Casablanca'
const timeZone = 'Africa/Casablanca'; const currentDate = dayjs().format('YYYY-MM-DD')
const currentDate = dayjs().format('YYYY-MM-DD');
return dayjs
return dayjs.tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone).format('YYYY-MM-DDTHH:mm:ssZ'); .tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone)
} .format('YYYY-MM-DDTHH:mm:ssZ')
}

View file

@ -1,60 +1,58 @@
const { parser, url } = require('./chada.ma.config.js') const { parser, url } = require('./chada.ma.config.js')
const axios = require('axios') const dayjs = require('dayjs')
const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc')
const cheerio = require('cheerio') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(customParseFormat)
dayjs.extend(timezone)
dayjs.extend(customParseFormat) jest.mock('axios')
jest.mock('axios') const mockHtmlContent = `
<div class="pm0 col-md-8" id="stopfix">
const mockHtmlContent = ` <h2 class="posts-date">Programmes d'Aujourd'hui</h2>
<div class="pm0 col-md-8" id="stopfix"> <div class="posts-area">
<h2 class="posts-date">Programmes d'Aujourd'hui</h2> <h2> <i class="fas fa-circle"></i>00:00 - 09:00</h2>
<div class="posts-area"> <div class="relativeme">
<h2> <i class="fas fa-circle"></i>00:00 - 09:00</h2> <a href="https://chada.ma/fr/emissions/bloc-prime-clips/">
<div class="relativeme"> <img class="programthumb" src="https://chada.ma/wp-content/uploads/2023/11/Autres-slides-clips-la-couverture.jpg">
<a href="https://chada.ma/fr/emissions/bloc-prime-clips/"> </a>
<img class="programthumb" src="https://chada.ma/wp-content/uploads/2023/11/Autres-slides-clips-la-couverture.jpg"> </div>
</a> <h3>Bloc Prime + Clips</h3>
</div> <div class="authorbox"></div>
<h3>Bloc Prime + Clips</h3> <div class="ssprogramme row"></div>
<div class="authorbox"></div> </div>
<div class="ssprogramme row"></div> </div>
</div> `
</div>
`; it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
it('can generate valid url', () => { })
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
}); it('can parse response', () => {
const content = mockHtmlContent
it('can parse response', () => {
const content = mockHtmlContent const result = parser({ content }).map(p => {
p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
const result = parser({ content }).map(p => { p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') return p
p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') })
return p
}) expect(result).toMatchObject([
{
expect(result).toMatchObject([ title: 'Bloc Prime + Clips',
{ description: 'No description available',
title: "Bloc Prime + Clips", start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'),
description: "No description available", stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'), }
stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') ])
} })
])
}) it('can handle empty guide', () => {
const result = parser({
it('can handle empty guide', () => { content: '<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>'
const result = parser({ })
content: '<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>' expect(result).toMatchObject([])
}) })
expect(result).toMatchObject([])
})

View file

@ -16,9 +16,12 @@ module.exports = {
const start = parseStart(item) const start = parseStart(item)
const stop = parseStop(item, start) const stop = parseStop(item, start)
programs.push({ programs.push({
title: item.season?.serie?.title ? item.season.serie.title : item.title, title: item.title,
subTitle: item.season?.serie?.title,
category: item.genreDetailed, category: item.genreDetailed,
description: item.synopsis, description: item.synopsis,
season: parseSeason(item),
episode: parseEpisode(item),
image: parseImage(item), image: parseImage(item),
start: start.toJSON(), start: start.toJSON(),
stop: stop.toJSON() stop: stop.toJSON()
@ -29,7 +32,7 @@ module.exports = {
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get(`https://chaines-tv.orange.fr/programme-tv?filtres=all`) .get('https://chaines-tv.orange.fr/programme-tv?filtres=all')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
@ -61,6 +64,14 @@ function parseStop(item, start) {
return start.add(item.duration, 's') return start.add(item.duration, 's')
} }
function parseSeason(item) {
return item.season?.number
}
function parseEpisode(item) {
return item.episodeNumber
}
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)

View file

@ -27,6 +27,9 @@ it('can parse response', () => {
start: '2021-11-07T23:35:00.000Z', start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z', stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste', title: 'Tête de liste',
subTitle: 'Esprits criminels',
season: 10,
episode: 12,
description: description:
"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.",
category: 'Série Suspense', category: 'Série Suspense',

View file

@ -40,7 +40,7 @@ module.exports = {
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get(`https://contenthub-api.eco.astro.com.my/channel/all.json`) .get('https://contenthub-api.eco.astro.com.my/channel/all.json')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
@ -85,7 +85,7 @@ function parseItems(content, date) {
const schedules = data.response.schedule const schedules = data.response.schedule
return schedules[date.format('YYYY-MM-DD')] || [] return schedules[date.format('YYYY-MM-DD')] || []
} catch (e) { } catch {
return [] return []
} }
} }

View file

@ -1,81 +1,85 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
module.exports = { module.exports = {
site: 'cosmotetv.gr', site: 'cosmotetv.gr',
days: 5, days: 5,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
}, },
method: 'GET', method: 'GET',
headers: { headers: {
'referer': 'https://www.cosmotetv.gr/', referer: 'https://www.cosmotetv.gr/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'User-Agent':
'Accept': '*/*', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9', Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'en-US,en;q=0.9',
'Origin': 'https://www.cosmotetv.gr', 'Accept-Encoding': 'gzip, deflate, br, zstd',
'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"', Origin: 'https://www.cosmotetv.gr',
'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"',
'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Ch-Ua-Mobile': '?0',
'Sec-Fetch-Dest': 'empty', 'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Site': 'cross-site' 'Sec-Fetch-Mode': 'cors',
} 'Sec-Fetch-Site': 'cross-site'
}, }
url: function ({date, channel}) { },
const startOfDay = dayjs(date).startOf('day').utc().unix() url: function ({ date, channel }) {
const endOfDay = dayjs(date).endOf('day').utc().unix() const startOfDay = dayjs(date).startOf('day').utc().unix()
return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` const endOfDay = dayjs(date).endOf('day').utc().unix()
}, return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
parser: function ({ date, content }) { },
let programs = [] parser: function ({ content }) {
const data = JSON.parse(content) let programs = []
data.channels.forEach(channel => { const data = JSON.parse(content)
channel.items.forEach(item => { data.channels.forEach(channel => {
const start = dayjs(item.startTime).utc().toISOString() channel.items.forEach(item => {
const stop = dayjs(item.endTime).utc().toISOString() const start = dayjs(item.startTime).utc().toISOString()
programs.push({ const stop = dayjs(item.endTime).utc().toISOString()
title: item.title, programs.push({
description: item.description || 'No description available', title: item.title,
category: item.qoe.genre, description: item.description || 'No description available',
image: item.thumbnails.standard, category: item.qoe.genre,
start, image: item.thumbnails.standard,
stop start,
}) stop
}) })
}) })
return programs })
}, return programs
async channels() { },
const axios = require('axios') async channels() {
try { const axios = require('axios')
const response = await axios.get('https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el', { try {
headers: this.request.headers const response = await axios.get(
}) 'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el',
const data = response.data {
headers: this.request.headers
if (data && data.channels) { }
return data.channels.map(item => ({ )
lang: 'el', const data = response.data
site_id: item.callSign,
name: item.title, if (data && data.channels) {
//logo: item.logos.square return data.channels.map(item => ({
})) lang: 'el',
} else { site_id: item.callSign,
console.error('Unexpected response structure:', data) name: item.title
return [] //logo: item.logos.square
} }))
} catch (error) { } else {
console.error('Error fetching channel data:', error) console.error('Unexpected response structure:', data)
return [] return []
} }
} } catch (error) {
} console.error('Error fetching channel data:', error)
return []
}
}
}

View file

@ -1,81 +1,76 @@
const { parser, url, channels } = require('./cosmotetv.gr.config.js') const { parser, url } = require('./cosmotetv.gr.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const axios = require('axios')
dayjs.extend(utc)
dayjs.extend(utc) dayjs.extend(customParseFormat)
dayjs.extend(customParseFormat) dayjs.extend(timezone)
dayjs.extend(timezone)
jest.mock('axios')
jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d') const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
const mockEpgData = {
const mockChannelData = { channels: [
"channels": [ {
{ items: [
"guid": "XTV100000954", {
"title": "ΒΟΥΛΗ HD", startTime: '2024-12-26T23:00:00+00:00',
"callSign": "vouli", endTime: '2024-12-27T00:00:00+00:00',
"logos": { title: 'Τι Λέει ο Νόμος',
"square": "https://tr.static.cdn.cosmotetvott.gr/ote-prod/channel_logos/vouli1-normal.png", description:
"wide": "https://tr.static.cdn.cosmotetvott.gr/ote-prod/channel_logos/vouli1-wide.png" 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
} qoe: {
} genre: 'Special'
] },
} thumbnails: {
standard:
const mockEpgData = { 'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg'
"channels": [ }
{ }
"items": [ ]
{ }
"startTime": "2024-12-26T23:00:00+00:00", ]
"endTime": "2024-12-27T00:00:00+00:00", }
"title": "Τι Λέει ο Νόμος",
"description": "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.", it('can generate valid url', () => {
"qoe": { const startOfDay = dayjs(date).startOf('day').utc().unix()
"genre": "Special" const endOfDay = dayjs(date).endOf('day').utc().unix()
}, expect(url({ date, channel })).toBe(
"thumbnails": { `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
"standard": "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg" )
} })
}
] it('can parse response', () => {
} const content = JSON.stringify(mockEpgData)
] const result = parser({ date, content }).map(p => {
} p.start = dayjs(p.start).toISOString()
p.stop = dayjs(p.stop).toISOString()
it('can generate valid url', () => { return p
const startOfDay = dayjs(date).startOf('day').utc().unix() })
const endOfDay = dayjs(date).endOf('day').utc().unix()
expect(url({ date, channel })).toBe(`https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`) expect(result).toMatchObject([
}) {
title: 'Τι Λέει ο Νόμος',
it('can parse response', () => { description:
const content = JSON.stringify(mockEpgData) 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
const result = parser({ date, content }).map(p => { category: 'Special',
p.start = dayjs(p.start).toISOString() image:
p.stop = dayjs(p.stop).toISOString() 'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg',
return p start: '2024-12-26T23:00:00.000Z',
}) stop: '2024-12-27T00:00:00.000Z'
}
expect(result).toMatchObject([ ])
{ })
title: "Τι Λέει ο Νόμος",
description: "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.", it('can handle empty guide', () => {
category: "Special", const result = parser({
image: "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg", date,
start: "2024-12-26T23:00:00.000Z", channel,
stop: "2024-12-27T00:00:00.000Z" content: '{"date":"2024-12-26","categories":[],"channels":[]}'
} })
]) expect(result).toMatchObject([])
}) })
it('can handle empty guide', () => {
const result = parser({ date, channel, content: '{"date":"2024-12-26","categories":[],"channels":[]}' });
expect(result).toMatchObject([])
})

View file

@ -1,102 +1,114 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'cubmu.com', site: 'cubmu.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format('YYYY-MM-DD')}&channel_id=${channel.site_id}` return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format(
}, 'YYYY-MM-DD'
parser({ content, channel }) { )}&channel_id=${channel.site_id}`
const programs = [] },
const items = parseItems(content) parser({ content, channel }) {
items.forEach(item => { const programs = []
programs.push({ const items = parseItems(content)
title: parseTitle(item), items.forEach(item => {
description: parseDescription(item, channel.lang), programs.push({
episode: parseEpisode(item), title: parseTitle(item),
start: parseStart(item).toISOString(), description: parseDescription(item, channel.lang),
stop: parseStop(item).toISOString() episode: parseEpisode(item),
}) start: parseStart(item).toISOString(),
}) stop: parseStop(item).toISOString()
})
return programs })
},
async channels({ lang = 'id' }) { return programs
const axios = require('axios') },
const cheerio = require('cheerio') async channels({ lang = 'id' }) {
const result = await axios const axios = require('axios')
.get('https://cubmu.com/live-tv') const cheerio = require('cheerio')
.then(response => response.data) const result = await axios
.catch(console.error) .get('https://cubmu.com/live-tv')
.then(response => response.data)
const $ = cheerio.load(result) .catch(console.error)
// retrieve service api data const $ = cheerio.load(result)
const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {}
// retrieve service api data
const options = { const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {}
headers: {
Origin: 'https://cubmu.com', const options = {
Referer: 'https://cubmu.com/live-tv' headers: {
} Origin: 'https://cubmu.com',
} Referer: 'https://cubmu.com/live-tv'
// login to service bus }
const token = await axios }
.post(`https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`, options) // login to service bus
.then(response => response.data) await axios
.catch(console.error) .post(
// list channels `https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`,
const subscribedChannels = await axios options
.post(`https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`, options) )
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
// list channels
const channels = [] const subscribedChannels = await axios
const included = [] .post(
if (Array.isArray(subscribedChannels.channelPackageList)) { `https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`,
subscribedChannels.channelPackageList.forEach(pkg => { options
pkg.channelList.forEach(channel => { )
if (included.indexOf(channel.id) < 0) { .then(response => response.data)
included.push(channel.id) .catch(console.error)
channels.push({
lang, const channels = []
site_id: channel.id, const included = []
name: channel.name if (Array.isArray(subscribedChannels.channelPackageList)) {
}) subscribedChannels.channelPackageList.forEach(pkg => {
} pkg.channelList.forEach(channel => {
}) if (included.indexOf(channel.id) < 0) {
}) included.push(channel.id)
} channels.push({
lang,
return channels site_id: channel.id,
} name: channel.name
} })
}
function parseItems(content) { })
return content ? JSON.parse(content.trim()).result || [] : [] })
} }
function parseTitle(item) { return channels
return item.scehedule_title }
} }
function parseDescription(item, lang = 'id') { function parseItems(content) {
return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis return content ? JSON.parse(content.trim()).result || [] : []
} }
function parseEpisode(item) { function parseTitle(item) {
return item.schedule_json.episodeName return item.scehedule_title
} }
function parseStart(item) { function parseDescription(item, lang = 'id') {
return dayjs.tz(item.schedule_date, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis
} }
function parseStop(item) { function parseEpisode(item) {
return dayjs.tz([item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '), 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') return item.schedule_json.episodeName
} }
function parseStart(item) {
return dayjs.tz(item.schedule_date, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
}
function parseStop(item) {
return dayjs.tz(
[item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '),
'YYYY-MM-DD HH:mm:ss',
'Asia/Jakarta'
)
}

View file

@ -1,47 +1,47 @@
const { url, parser } = require('./cubmu.com.config.js') const { url, parser } = require('./cubmu.com.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d') const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' } const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' }
const channelEn = Object.assign({}, channel, { lang: 'en' }) const channelEn = Object.assign({}, channel, { lang: 'en' })
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=2023-11-05&channel_id=4028c68574537fcd0174be43042758d8' 'https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=2023-11-05&channel_id=4028c68574537fcd0174be43042758d8'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content =
'{"result":[{"channel_id":"4028c68574537fcd0174be43042758d8","channel_name":"Trans TV","scehedule_title":"CNN Tech News","schedule_date":"2023-11-05 01:30:00","schedule_end_time":"02:00:00","schedule_json":{"availability":0,"channelId":"4028c68574537fcd0174be43042758d8","channelName":"Trans TV","duration":1800,"editable":true,"episodeName":"","imageUrl":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/458x640","imageUrlWide":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/320x180","name":"CNN Tech News","ottImageUrl":"","primarySynopsis":"CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.","scheduleId":"4028c6858b8b3621018b9330e3701a7e","scheduleTime":"18:30:00","secondarySynopsis":"CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.","startDt":"20231104183000","url":""},"schedule_start_time":"01:30:00"}]}' '{"result":[{"channel_id":"4028c68574537fcd0174be43042758d8","channel_name":"Trans TV","scehedule_title":"CNN Tech News","schedule_date":"2023-11-05 01:30:00","schedule_end_time":"02:00:00","schedule_json":{"availability":0,"channelId":"4028c68574537fcd0174be43042758d8","channelName":"Trans TV","duration":1800,"editable":true,"episodeName":"","imageUrl":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/458x640","imageUrlWide":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/320x180","name":"CNN Tech News","ottImageUrl":"","primarySynopsis":"CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.","scheduleId":"4028c6858b8b3621018b9330e3701a7e","scheduleTime":"18:30:00","secondarySynopsis":"CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.","startDt":"20231104183000","url":""},"schedule_start_time":"01:30:00"}]}'
const idResults = parser({ content, channel }) const idResults = parser({ content, channel })
expect(idResults).toMatchObject([ expect(idResults).toMatchObject([
{ {
start: '2023-11-04T18:30:00.000Z', start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z', stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News', title: 'CNN Tech News',
description: description:
'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.' 'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.'
} }
]) ])
const enResults = parser({ content, channel: channelEn }) const enResults = parser({ content, channel: channelEn })
expect(enResults).toMatchObject([ expect(enResults).toMatchObject([
{ {
start: '2023-11-04T18:30:00.000Z', start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z', stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News', title: 'CNN Tech News',
description: description:
'CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.' 'CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '' }) const results = parser({ content: '' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

@ -1,60 +1,59 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const cheerio = require('cheerio')
dayjs.extend(utc)
dayjs.extend(utc) dayjs.extend(timezone)
dayjs.extend(timezone) dayjs.extend(customParseFormat)
dayjs.extend(customParseFormat)
module.exports = {
module.exports = { site: 'cyta.com.cy',
site: 'cyta.com.cy', days: 7,
days: 7, request: {
request: { cache: {
cache: { ttl: 60 * 60 * 1000 // 1 hour
ttl: 60 * 60 * 1000 // 1 hour }
} },
}, url: function ({ date, channel }) {
url: function ({date, channel}) { // Get the epoch timestamp
// Get the epoch timestamp const todayEpoch = date.startOf('day').utc().valueOf()
const todayEpoch = date.startOf('day').utc().valueOf() // Get the epoch timestamp for the next day
// Get the epoch timestamp for the next day const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf()
const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() return `https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=${todayEpoch}&endTimeEpoch=${nextDayEpoch}&language=1&channelIds=${channel.site_id}`
return `https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=${todayEpoch}&endTimeEpoch=${nextDayEpoch}&language=1&channelIds=${channel.site_id}` },
}, parser: function ({ content }) {
parser: function ({content}) { const data = JSON.parse(content)
const data = JSON.parse(content) const programs = []
const programs = []
data.channelEpgs.forEach(channel => {
data.channelEpgs.forEach(channel => { channel.epgPlayables.forEach(epg => {
channel.epgPlayables.forEach(epg => { const start = new Date(epg.startTime).toISOString()
const start = new Date(epg.startTime).toISOString(); const stop = new Date(epg.endTime).toISOString()
const stop = new Date(epg.endTime).toISOString();
programs.push({
programs.push({ title: epg.name,
title: epg.name, start,
start, stop
stop })
}) })
}) })
})
return programs
return programs },
}, async channels() {
async channels() { const axios = require('axios')
const axios = require('axios') const data = await axios
const data = await axios .get('https://epg.cyta.com.cy/api/mediacatalog/fetchChannels?language=1')
.get(`https://epg.cyta.com.cy/api/mediacatalog/fetchChannels?language=1`) .then(r => r.data)
.then(r => r.data) .catch(console.log)
.catch(console.log)
return data.channels.map(item => {
return data.channels.map(item => { return {
return { lang: 'el',
lang: 'el', site_id: item.id,
site_id: item.id, name: item.name
name: item.name }
} })
}) }
} }
}

View file

@ -1,53 +1,49 @@
const { url, parser } = require('./cyta.com.cy.config.js') const { url, parser } = require('./cyta.com.cy.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day')
const channel = { const channel = {
site_id: '561066', site_id: '561066',
xmltv_id: 'RIK1.cy' xmltv_id: 'RIK1.cy'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const generatedUrl = url({ date, channel }) const generatedUrl = url({ date, channel })
expect(generatedUrl).toBe( expect(generatedUrl).toBe(
'https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=1735862400000&endTimeEpoch=1735948800000&language=1&channelIds=561066' 'https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=1735862400000&endTimeEpoch=1735948800000&language=1&channelIds=561066'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = ` const content = `
{ {
"channelEpgs": [ "channelEpgs": [
{ {
"epgPlayables": [ "epgPlayables": [
{ "name": "Πρώτη Ενημέρωση", "startTime": 1735879500000, "endTime": 1735889400000 } { "name": "Πρώτη Ενημέρωση", "startTime": 1735879500000, "endTime": 1735889400000 }
] ]
} }
] ]
}` }`
const result = parser({ content }).map(p => { const result = parser({ content })
p.start = p.start
p.stop = p.stop expect(result).toMatchObject([
return p {
}) title: 'Πρώτη Ενημέρωση',
start: '2025-01-03T04:45:00.000Z',
expect(result).toMatchObject([ stop: '2025-01-03T07:30:00.000Z'
{ }
title: 'Πρώτη Ενημέρωση', ])
start: '2025-01-03T04:45:00.000Z', })
stop: '2025-01-03T07:30:00.000Z'
} it('can handle empty guide', () => {
]) const result = parser({
}) content: '{"channelEpgs":[]}'
})
it('can handle empty guide', () => { expect(result).toMatchObject([])
const result = parser({ })
content: '{"channelEpgs":[]}'
})
expect(result).toMatchObject([])
})

View file

@ -13,9 +13,9 @@ module.exports = {
site: 'dens.tv', site: 'dens.tv',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format('YYYY-MM-DD')}&id_channel=${ return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format(
channel.site_id 'YYYY-MM-DD'
}&app_type=10` )}&id_channel=${channel.site_id}&app_type=10`
}, },
parser({ content }) { parser({ content }) {
// parsing // parsing
@ -25,8 +25,9 @@ module.exports = {
if (Array.isArray(response?.data)) { if (Array.isArray(response?.data)) {
response.data.forEach(item => { response.data.forEach(item => {
const title = item.title const title = item.title
const [, , , season, , , episode] = title.match(/( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/) || const [, , , season, , , episode] = title.match(
[null, null, null, null, null, null, null] /( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/
) || [null, null, null, null, null, null, null]
programs.push({ programs.push({
title, title,
description: item.description, description: item.description,
@ -52,7 +53,7 @@ module.exports = {
const channels = [] const channels = []
for (const id_category of Object.values(categories)) { for (const id_category of Object.values(categories)) {
const data = await axios const data = await axios
.get(`https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory`, { .get('https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory', {
params: { id_category } params: { id_category }
}) })
.then(r => r.data) .then(r => r.data)

View file

@ -10,7 +10,9 @@ const date = dayjs.utc('2024-11-24').startOf('d')
const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' } const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10') expect(url({ channel, date })).toBe(
'https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10'
)
}) })
it('can parse response', () => { it('can parse response', () => {

View file

@ -65,7 +65,7 @@ module.exports = {
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://www.digiturk.com.tr/`, { .get('https://www.digiturk.com.tr/', {
headers: { headers: {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'

View file

@ -60,7 +60,7 @@ module.exports = {
$('.pgrid').each((i, el) => { $('.pgrid').each((i, el) => {
const onclick = $(el).find('.chnl-logo').attr('onclick') const onclick = $(el).find('.chnl-logo').attr('onclick')
const number = $(el).find('.cnl-fav > a > span').text().trim() const number = $(el).find('.cnl-fav > a > span').text().trim()
const [, name, site_id] = onclick.match(/ShowChannelGuid\('([^']+)','([^']+)'/) || [ const [, , site_id] = onclick.match(/ShowChannelGuid\('([^']+)','([^']+)'/) || [
null, null,
'', '',
'' ''

View file

@ -49,7 +49,7 @@ module.exports = {
const channels = [] const channels = []
for (let provider of providers) { for (let provider of providers) {
const data = await axios const data = await axios
.post(`https://www.guida.tv/guide/schedule`, null, { .post('https://www.guida.tv/guide/schedule', null, {
params: { params: {
provider, provider,
region: 'Italy', region: 'Italy',
@ -81,7 +81,7 @@ module.exports = {
} }
} }
function parseStart($item, date, channel) { function parseStart($item, date) {
const timeString = $item('td:eq(0)').text().trim() const timeString = $item('td:eq(0)').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`

View file

@ -35,7 +35,7 @@ module.exports = {
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://guidatv.sky.it/canali`) .get('https://guidatv.sky.it/canali')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -1,63 +1,63 @@
const axios = require('axios') const axios = require('axios')
const convert = require('xml-js') const convert = require('xml-js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(timezone) dayjs.extend(timezone)
module.exports = { module.exports = {
site: 'hoy.tv', site: 'hoy.tv',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1h ttl: 60 * 60 * 1000 // 1h
} }
}, },
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `https://epg-file.hoy.tv/hoy/OTT${channel.site_id}${date.format('YYYYMMDD')}.xml` return `https://epg-file.hoy.tv/hoy/OTT${channel.site_id}${date.format('YYYYMMDD')}.xml`
}, },
parser({ content, channel, date }) { parser({ content, date }) {
const data = convert.xml2js(content, { const data = convert.xml2js(content, {
compact: true, compact: true,
ignoreDeclaration: true, ignoreDeclaration: true,
ignoreAttributes: true ignoreAttributes: true
}) })
const programs = [] const programs = []
for (let item of data.ProgramGuide.Channel.EpgItem) { for (let item of data.ProgramGuide.Channel.EpgItem) {
const start = dayjs.tz(item.EpgStartDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong') const start = dayjs.tz(item.EpgStartDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong')
if (! date.isSame(start, 'day')) { if (!date.isSame(start, 'day')) {
continue continue
} }
const epIndex = item.EpisodeInfo.EpisodeIndex._text const epIndex = item.EpisodeInfo.EpisodeIndex._text
const subtitle = parseInt(epIndex) > 0 ? `${epIndex}` : undefined const subtitle = parseInt(epIndex) > 0 ? `${epIndex}` : undefined
programs.push({ programs.push({
title: `${item.ComScore.ns_st_pr._text}${item.EpgOtherInfo?._text || ''}`, title: `${item.ComScore.ns_st_pr._text}${item.EpgOtherInfo?._text || ''}`,
sub_title: subtitle, sub_title: subtitle,
description: item.EpisodeInfo.EpisodeLongDescription._text, description: item.EpisodeInfo.EpisodeLongDescription._text,
start, start,
stop: dayjs.tz(item.EpgEndDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong'), stop: dayjs.tz(item.EpgEndDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong')
}) })
} }
return programs return programs
}, },
async channels({ lang }) { async channels() {
const data = await axios const data = await axios
.get('https://api2.hoy.tv/api/v2/a/channel') .get('https://api2.hoy.tv/api/v2/a/channel')
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.data.map(c => { return data.data.map(c => {
return { return {
site_id: c.videos.id, site_id: c.videos.id,
name: c.name.zh_hk, name: c.name.zh_hk,
lang: 'zh', lang: 'zh'
} }
}) })
} }
} }

View file

@ -1,116 +1,115 @@
const { parser, url } = require('./hoy.tv.config.js') const { parser, url } = require('./hoy.tv.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '76', site_id: '76',
xmltv_id: 'HOYIBC.hk', xmltv_id: 'HOYIBC.hk',
lang: 'zh' lang: 'zh'
} }
const content = `<?xml version="1.0" encoding="UTF-8" ?> const content = `<?xml version="1.0" encoding="UTF-8" ?>
<ProgramGuide> <ProgramGuide>
<Channel id="76"> <Channel id="76">
<EpgItem> <EpgItem>
<EpgStartDateTime>2024-09-13 11:30:00</EpgStartDateTime> <EpgStartDateTime>2024-09-13 11:30:00</EpgStartDateTime>
<EpgEndDateTime>2024-09-13 12:30:00</EpgEndDateTime> <EpgEndDateTime>2024-09-13 12:30:00</EpgEndDateTime>
<EpgOtherInfo>[PG]</EpgOtherInfo> <EpgOtherInfo>[PG]</EpgOtherInfo>
<DisableLive>false</DisableLive> <DisableLive>false</DisableLive>
<DisableVod>false</DisableVod> <DisableVod>false</DisableVod>
<VODLicPeriod>2024-09-27 11:30:00</VODLicPeriod> <VODLicPeriod>2024-09-27 11:30:00</VODLicPeriod>
<ProgramInfo> <ProgramInfo>
<ProgramId>0</ProgramId> <ProgramId>0</ProgramId>
<ProgramTitle></ProgramTitle> <ProgramTitle></ProgramTitle>
<ProgramPos>0</ProgramPos> <ProgramPos>0</ProgramPos>
<FirstRunDateTime></FirstRunDateTime> <FirstRunDateTime></FirstRunDateTime>
<ProgramThumbnailUrl>http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg</ProgramThumbnailUrl> <ProgramThumbnailUrl>http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg</ProgramThumbnailUrl>
</ProgramInfo> </ProgramInfo>
<EpisodeInfo> <EpisodeInfo>
<EpisodeId>EQ00135</EpisodeId> <EpisodeId>EQ00135</EpisodeId>
<EpisodeIndex>46</EpisodeIndex> <EpisodeIndex>46</EpisodeIndex>
<EpisodeShortDescription>點講都係一家人</EpisodeShortDescription> <EpisodeShortDescription>點講都係一家人</EpisodeShortDescription>
<EpisodeLongDescription></EpisodeLongDescription> <EpisodeLongDescription></EpisodeLongDescription>
<EpisodeThumbnailUrl>http://tv.fantv.hk/images/nosuchthumbnail.jpg</EpisodeThumbnailUrl> <EpisodeThumbnailUrl>http://tv.fantv.hk/images/nosuchthumbnail.jpg</EpisodeThumbnailUrl>
</EpisodeInfo> </EpisodeInfo>
<ComScore> <ComScore>
<ns_st_stc></ns_st_stc> <ns_st_stc></ns_st_stc>
<ns_st_pr>點講都係一家人</ns_st_pr> <ns_st_pr>點講都係一家人</ns_st_pr>
<ns_st_tpr>0</ns_st_tpr> <ns_st_tpr>0</ns_st_tpr>
<ns_st_tep>EQ00135</ns_st_tep> <ns_st_tep>EQ00135</ns_st_tep>
<ns_st_ep>點講都係一家人 Episode 46</ns_st_ep> <ns_st_ep>點講都係一家人 Episode 46</ns_st_ep>
<ns_st_li>1</ns_st_li> <ns_st_li>1</ns_st_li>
<ns_st_tdt>20240913</ns_st_tdt> <ns_st_tdt>20240913</ns_st_tdt>
<ns_st_tm>1130</ns_st_tm> <ns_st_tm>1130</ns_st_tm>
<ns_st_ty>0001</ns_st_ty> <ns_st_ty>0001</ns_st_ty>
<ns_st_cl>3704000</ns_st_cl> <ns_st_cl>3704000</ns_st_cl>
</ComScore> </ComScore>
</EpgItem> </EpgItem>
<EpgItem> <EpgItem>
<EpgStartDateTime>2024-09-13 12:30:00</EpgStartDateTime> <EpgStartDateTime>2024-09-13 12:30:00</EpgStartDateTime>
<EpgEndDateTime>2024-09-13 13:30:00</EpgEndDateTime> <EpgEndDateTime>2024-09-13 13:30:00</EpgEndDateTime>
<EpgOtherInfo></EpgOtherInfo> <EpgOtherInfo></EpgOtherInfo>
<DisableLive>false</DisableLive> <DisableLive>false</DisableLive>
<DisableVod>false</DisableVod> <DisableVod>false</DisableVod>
<VODLicPeriod>2024-09-27 12:30:00</VODLicPeriod> <VODLicPeriod>2024-09-27 12:30:00</VODLicPeriod>
<ProgramInfo> <ProgramInfo>
<ProgramId>0</ProgramId> <ProgramId>0</ProgramId>
<ProgramTitle></ProgramTitle> <ProgramTitle></ProgramTitle>
<ProgramPos>0</ProgramPos> <ProgramPos>0</ProgramPos>
<FirstRunDateTime></FirstRunDateTime> <FirstRunDateTime></FirstRunDateTime>
<ProgramThumbnailUrl>http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg</ProgramThumbnailUrl> <ProgramThumbnailUrl>http://tv.fantv.hk/images/thumbnail_1920_1080_fantv.jpg</ProgramThumbnailUrl>
</ProgramInfo> </ProgramInfo>
<EpisodeInfo> <EpisodeInfo>
<EpisodeId>ED00311</EpisodeId> <EpisodeId>ED00311</EpisodeId>
<EpisodeIndex>0</EpisodeIndex> <EpisodeIndex>0</EpisodeIndex>
<EpisodeShortDescription>麝香之路</EpisodeShortDescription> <EpisodeShortDescription>麝香之路</EpisodeShortDescription>
<EpisodeLongDescription>Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world</EpisodeLongDescription> <EpisodeLongDescription>Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world</EpisodeLongDescription>
<EpisodeThumbnailUrl>http://tv.fantv.hk/images/nosuchthumbnail.jpg</EpisodeThumbnailUrl> <EpisodeThumbnailUrl>http://tv.fantv.hk/images/nosuchthumbnail.jpg</EpisodeThumbnailUrl>
</EpisodeInfo> </EpisodeInfo>
<ComScore> <ComScore>
<ns_st_stc></ns_st_stc> <ns_st_stc></ns_st_stc>
<ns_st_pr>麝香之路</ns_st_pr> <ns_st_pr>麝香之路</ns_st_pr>
<ns_st_tpr>0</ns_st_tpr> <ns_st_tpr>0</ns_st_tpr>
<ns_st_tep>ED00311</ns_st_tep> <ns_st_tep>ED00311</ns_st_tep>
<ns_st_ep>麝香之路 2024-09-13</ns_st_ep> <ns_st_ep>麝香之路 2024-09-13</ns_st_ep>
<ns_st_li>1</ns_st_li> <ns_st_li>1</ns_st_li>
<ns_st_tdt>20240913</ns_st_tdt> <ns_st_tdt>20240913</ns_st_tdt>
<ns_st_tm>1230</ns_st_tm> <ns_st_tm>1230</ns_st_tm>
<ns_st_ty>0001</ns_st_ty> <ns_st_ty>0001</ns_st_ty>
<ns_st_cl>3704000</ns_st_cl> <ns_st_cl>3704000</ns_st_cl>
</ComScore> </ComScore>
</EpgItem> </EpgItem>
</Channel> </Channel>
</ProgramGuide>` </ProgramGuide>`
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml')
'https://epg-file.hoy.tv/hoy/OTT7620240913.xml' })
)
}) it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => {
it('can parse response', () => { p.start = p.start.toJSON()
const result = parser({ content, channel, date }).map(p => { p.stop = p.stop.toJSON()
p.start = p.start.toJSON() return p
p.stop = p.stop.toJSON() })
return p
}) expect(result).toMatchObject([
{
expect(result).toMatchObject([ start: '2024-09-13T03:30:00.000Z',
{ stop: '2024-09-13T04:30:00.000Z',
start: '2024-09-13T03:30:00.000Z', title: '點講都係一家人[PG]',
stop: '2024-09-13T04:30:00.000Z', sub_title: '第46集'
title: '點講都係一家人[PG]', },
sub_title: '第46集', {
}, start: '2024-09-13T04:30:00.000Z',
{ stop: '2024-09-13T05:30:00.000Z',
start: '2024-09-13T04:30:00.000Z', title: '麝香之路',
stop: '2024-09-13T05:30:00.000Z', description:
title: '麝香之路', 'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world'
description: 'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world', }
} ])
]) })
})

View file

@ -172,14 +172,17 @@ function parseItems(content, channel, date) {
if (!data || !Array.isArray(data.programs)) return [] if (!data || !Array.isArray(data.programs)) return []
return data.programs return data.programs
.filter(p => p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day)) .filter(
p =>
p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day)
)
.map(p => { .map(p => {
if (Array.isArray(p.date) && p.date.length) { if (Array.isArray(p.date) && p.date.length) {
p.date = p.date[0] p.date = p.date[0]
} }
return p return p
}) })
} catch (error) { } catch {
return [] return []
} }
} }

View file

@ -1,73 +1,80 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'ipko.tv', site: 'ipko.tv',
timezone: 'Europe/Belgrade', timezone: 'Europe/Belgrade',
days: 5, days: 5,
url({ date, channel }) { return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData' }, url() {
request: { return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData'
method: 'POST', },
headers: { request: {
'Host': 'stargate.ipko.tv', method: 'POST',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', headers: {
'Accept': 'application/json, text/plain, */*', Host: 'stargate.ipko.tv',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', 'User-Agent':
'Content-Type': 'application/json', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'X-AppLayout': '1', Accept: 'application/json, text/plain, */*',
'x-language': 'sq', 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Origin': 'https://ipko.tv', 'Content-Type': 'application/json',
'Sec-Fetch-Dest': 'empty', 'X-AppLayout': '1',
'Sec-Fetch-Mode': 'cors', 'x-language': 'sq',
'Sec-Fetch-Site': 'cross-site', Origin: 'https://ipko.tv',
'Sec-GPC': '1', 'Sec-Fetch-Dest': 'empty',
'Connection': 'keep-alive' 'Sec-Fetch-Mode': 'cors',
}, 'Sec-Fetch-Site': 'cross-site',
data({ channel, date }) { 'Sec-GPC': '1',
const todayEpoch = date.startOf('day').unix(); Connection: 'keep-alive'
const nextDayEpoch = date.add(1, 'day').startOf('day').unix(); },
return JSON.stringify({ data({ channel, date }) {
ch_ext_id: channel.site_id, const todayEpoch = date.startOf('day').unix()
from: todayEpoch, const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
to: nextDayEpoch return JSON.stringify({
}) ch_ext_id: channel.site_id,
} from: todayEpoch,
}, to: nextDayEpoch
parser: function ({ content }) { })
const programs = []; }
const data = JSON.parse(content); },
data.shows.forEach(show => { parser: function ({ content }) {
const start = dayjs.unix(show.show_start).utc(); const programs = []
const stop = dayjs.unix(show.show_end).utc(); const data = JSON.parse(content)
const programData = { data.shows.forEach(show => {
title: show.title, const start = dayjs.unix(show.show_start).utc()
description: show.summary || 'No description available', const stop = dayjs.unix(show.show_end).utc()
start: start.toISOString(), const programData = {
stop: stop.toISOString(), title: show.title,
thumbnail: show.thumbnail description: show.summary || 'No description available',
} start: start.toISOString(),
programs.push(programData) stop: stop.toISOString(),
}) thumbnail: show.thumbnail
return programs }
}, programs.push(programData)
async channels() { })
const response = await axios.post('https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList', JSON.stringify({ includeRadioStations: true }), { return programs
headers: this.request.headers },
}); async channels() {
const response = await axios.post(
const data = response.data.data; 'https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList',
return data.map(item => ({ JSON.stringify({ includeRadioStations: true }),
lang: 'sq', {
name: String(item.channel.title), headers: this.request.headers
site_id: String(item.channel.id), }
//logo: String(item.channel.logo) )
}))
} const data = response.data.data
} return data.map(item => ({
lang: 'sq',
name: String(item.channel.title),
site_id: String(item.channel.id)
//logo: String(item.channel.logo)
}))
}
}

View file

@ -1,115 +1,111 @@
const { parser, url } = require('./ipko.tv.config.js') const { parser, url } = require('./ipko.tv.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2024-12-24', 'YYYY-MM-DD').startOf('day') const date = dayjs.utc('2024-12-24', 'YYYY-MM-DD').startOf('day')
const channel = { const channel = {
site_id: 'ipko-promo', site_id: 'ipko-promo',
xmltv_id: 'IPKOPROMO' xmltv_id: 'IPKOPROMO'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData') expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = ` const content = `
{ {
"shows": [ "shows": [
{ {
"title": "IPKO Promo", "title": "IPKO Promo",
"show_start": 1735012800, "show_start": 1735012800,
"show_end": 1735020000, "show_end": 1735020000,
"timestamp": "5:00 - 7:00", "timestamp": "5:00 - 7:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105567", "show_id": "EPG_TvProfil_IPKOPROMO_296105567",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false, "is_adult": false,
"friendly_id": "ipko_promo_4cf3", "friendly_id": "ipko_promo_4cf3",
"pg": "", "pg": "",
"genres": [], "genres": [],
"year": 0, "year": 0,
"summary": "", "summary": "",
"categories": "Other", "categories": "Other",
"stb_only": false, "stb_only": false,
"is_live": false, "is_live": false,
"original_title": "IPKO Promo" "original_title": "IPKO Promo"
}, },
{ {
"title": "IPKO Promo", "title": "IPKO Promo",
"show_start": 1735020000, "show_start": 1735020000,
"show_end": 1735027200, "show_end": 1735027200,
"timestamp": "7:00 - 9:00", "timestamp": "7:00 - 9:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105568", "show_id": "EPG_TvProfil_IPKOPROMO_296105568",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false, "is_adult": false,
"friendly_id": "ipko_promo_416b", "friendly_id": "ipko_promo_416b",
"pg": "", "pg": "",
"genres": [], "genres": [],
"year": 0, "year": 0,
"summary": "", "summary": "",
"categories": "Other", "categories": "Other",
"stb_only": false, "stb_only": false,
"is_live": false, "is_live": false,
"original_title": "IPKO Promo" "original_title": "IPKO Promo"
}, },
{ {
"title": "IPKO Promo", "title": "IPKO Promo",
"show_start": 1735027200, "show_start": 1735027200,
"show_end": 1735034400, "show_end": 1735034400,
"timestamp": "9:00 - 11:00", "timestamp": "9:00 - 11:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105569", "show_id": "EPG_TvProfil_IPKOPROMO_296105569",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg", "thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false, "is_adult": false,
"friendly_id": "ipko_promo_2e23", "friendly_id": "ipko_promo_2e23",
"pg": "", "pg": "",
"genres": [], "genres": [],
"year": 0, "year": 0,
"summary": "", "summary": "",
"categories": "Other", "categories": "Other",
"stb_only": false, "stb_only": false,
"is_live": false, "is_live": false,
"original_title": "IPKO Promo" "original_title": "IPKO Promo"
} }
] ]
}` }`
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel })
p.start = p.start
p.stop = p.stop expect(result).toMatchObject([
return p {
}) title: 'IPKO Promo',
description: 'No description available',
expect(result).toMatchObject([ start: '2024-12-24T04:00:00.000Z',
{ stop: '2024-12-24T06:00:00.000Z',
title: "IPKO Promo", thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
description: "No description available", },
start: "2024-12-24T04:00:00.000Z", {
stop: "2024-12-24T06:00:00.000Z", title: 'IPKO Promo',
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg" description: 'No description available',
}, start: '2024-12-24T06:00:00.000Z',
{ stop: '2024-12-24T08:00:00.000Z',
title: "IPKO Promo", thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
description: "No description available", },
start: "2024-12-24T06:00:00.000Z", {
stop: "2024-12-24T08:00:00.000Z", title: 'IPKO Promo',
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg" description: 'No description available',
}, start: '2024-12-24T08:00:00.000Z',
{ stop: '2024-12-24T10:00:00.000Z',
title: "IPKO Promo", thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
description: "No description available", }
start: "2024-12-24T08:00:00.000Z", ])
stop: "2024-12-24T10:00:00.000Z", })
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg"
} it('can handle empty guide', () => {
]) const result = parser({
}) content: '{"shows":[]}'
})
it('can handle empty guide', () => { expect(result).toMatchObject([])
const result = parser({ })
content: '{"shows":[]}'
})
expect(result).toMatchObject([])
})

View file

@ -38,7 +38,7 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://m.tv.sms.cz/?zmen_stanice=true`) .get('https://m.tv.sms.cz/?zmen_stanice=true')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -77,8 +77,8 @@ function parseItems(content) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
console.log(error.message) return []
} }
if (!data || !Array.isArray(data)) return [] if (!data || !Array.isArray(data)) return []

View file

@ -32,7 +32,7 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/channels`) .get('https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/channels')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -56,7 +56,7 @@ function parseStop($item) {
try { try {
return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ')
} catch (err) { } catch {
return null return null
} }
} }

View file

@ -1,93 +1,97 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
module.exports = { module.exports = {
site: 'mediasetinfinity.mediaset.it', site: 'mediasetinfinity.mediaset.it',
days: 2, days: 2,
url: function ({date, channel}) { url: function ({ date, channel }) {
// Get the epoch timestamp // Get the epoch timestamp
const todayEpoch = date.startOf('day').utc().valueOf() const todayEpoch = date.startOf('day').utc().valueOf()
// Get the epoch timestamp for the next day // Get the epoch timestamp for the next day
const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf()
return `https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=${todayEpoch}~${nextDayEpoch}&byCallSign=${channel.site_id}` return `https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=${todayEpoch}~${nextDayEpoch}&byCallSign=${channel.site_id}`
}, },
parser: function ({content}) { parser: function ({ content }) {
const programs = [] const programs = []
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data.response || !data.response.entries || !data.response.entries[0] || !data.response.entries[0].listings) { if (
// If the structure is not as expected, return an empty array !data.response ||
return programs !data.response.entries ||
} !data.response.entries[0] ||
!data.response.entries[0].listings
const listings = data.response.entries[0].listings ) {
// If the structure is not as expected, return an empty array
listings.forEach((listing) => { return programs
const title = listing.mediasetlisting$epgTitle }
const subTitle = listing.program.title
const season = parseSeason(listing) const listings = data.response.entries[0].listings
const episode = parseEpisode(listing)
listings.forEach(listing => {
const title = listing.mediasetlisting$epgTitle
if (listing.program.title && listing.startTime && listing.endTime) { const subTitle = listing.program.title
programs.push({ const season = parseSeason(listing)
title: title || subTitle, const episode = parseEpisode(listing)
sub_title: title && title != subTitle ? subTitle : null,
description: listing.program.description || null, if (listing.program.title && listing.startTime && listing.endTime) {
category: listing.program.mediasetprogram$skyGenre || null, programs.push({
season: episode && !season ? '0' : season, title: title || subTitle,
episode: episode, sub_title: title && title != subTitle ? subTitle : null,
start: parseTime(listing.startTime), description: listing.program.description || null,
stop: parseTime(listing.endTime), category: listing.program.mediasetprogram$skyGenre || null,
image: getMaxResolutionThumbnails(listing) season: episode && !season ? '0' : season,
}) episode: episode,
} start: parseTime(listing.startTime),
}) stop: parseTime(listing.endTime),
image: getMaxResolutionThumbnails(listing)
return programs })
} }
} })
return programs
function parseTime(timestamp) { }
return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm') }
}
function parseTime(timestamp) {
function parseSeason(item) { return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm')
if (!item.mediasetlisting$shortDescription) return null }
const season = item.mediasetlisting$shortDescription.match(/S(\d+)\s/)
return season ? season[1] : null function parseSeason(item) {
} if (!item.mediasetlisting$shortDescription) return null
const season = item.mediasetlisting$shortDescription.match(/S(\d+)\s/)
function parseEpisode(item) { return season ? season[1] : null
if (!item.mediasetlisting$shortDescription) return null }
const episode = item.mediasetlisting$shortDescription.match(/Ep(\d+)\s/)
return episode ? episode[1] : null function parseEpisode(item) {
} if (!item.mediasetlisting$shortDescription) return null
const episode = item.mediasetlisting$shortDescription.match(/Ep(\d+)\s/)
function getMaxResolutionThumbnails(item) { return episode ? episode[1] : null
const thumbnails = item.program.thumbnails || null }
const maxResolutionThumbnails = {}
function getMaxResolutionThumbnails(item) {
for (const key in thumbnails) { const thumbnails = item.program.thumbnails || null
const type = key.split('-')[0] // Estrarre il tipo di thumbnail const maxResolutionThumbnails = {}
const {width, height, url, title} = thumbnails[key]
for (const key in thumbnails) {
if (!maxResolutionThumbnails[type] || const type = key.split('-')[0] // Estrarre il tipo di thumbnail
(width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height)) { const { width, height, url, title } = thumbnails[key]
maxResolutionThumbnails[type] = {width, height, url, title}
} if (
} !maxResolutionThumbnails[type] ||
if (maxResolutionThumbnails.image_keyframe_poster) width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height
return maxResolutionThumbnails.image_keyframe_poster.url ) {
else if (maxResolutionThumbnails.image_header_poster) maxResolutionThumbnails[type] = { width, height, url, title }
return maxResolutionThumbnails.image_header_poster.url }
else }
return null if (maxResolutionThumbnails.image_keyframe_poster)
} return maxResolutionThumbnails.image_keyframe_poster.url
else if (maxResolutionThumbnails.image_header_poster)
return maxResolutionThumbnails.image_header_poster.url
else return null
}

View file

@ -1,46 +1,53 @@
const {parser, url} = require('./mediasetinfinity.mediaset.it.config.js') const { parser, url } = require('./mediasetinfinity.mediaset.it.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2024-01-20', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-01-20', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'LB', xmltv_id: '20.it' site_id: 'LB',
} xmltv_id: '20.it'
}
it('can generate valid url', () => {
expect(url({ it('can generate valid url', () => {
channel, expect(
date url({
})).toBe('https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB') channel,
}) date
})
it('can parse response', () => { ).toBe(
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') 'https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB'
const results = parser({content, date}).map(p => { )
return p })
})
it('can parse response', () => {
expect(results[3]).toMatchObject({ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
start: '2024-01-20 02:14', const results = parser({ content, date }).map(p => {
stop: '2024-01-20 02:54', return p
title: 'Chicago Fire', })
sub_title: 'Ep. 22 - Io non ti lascio',
description: 'Severide e Kidd continuano a indagare su un vecchio caso doloso di Benny. Notizie inaspettate portano Brett a meditare su una grande decisione.', expect(results[3]).toMatchObject({
category: 'Intrattenimento', start: '2024-01-20 02:14',
season: '7', stop: '2024-01-20 02:54',
episode: '22', title: 'Chicago Fire',
image: 'https://static2.mediasetplay.mediaset.it/Mediaset_Italia_Production_-_Main/F309370301002204/media/0/0/1ef76b73-3173-43bd-9c16-73986a0ec131/46896726-11e7-4438-b947-d2ae53f58c0b.jpg' sub_title: 'Ep. 22 - Io non ti lascio',
}) description:
}) 'Severide e Kidd continuano a indagare su un vecchio caso doloso di Benny. Notizie inaspettate portano Brett a meditare su una grande decisione.',
category: 'Intrattenimento',
it('can handle empty guide', () => { season: '7',
const result = parser({ episode: '22',
content: '[]' image:
}) 'https://static2.mediasetplay.mediaset.it/Mediaset_Italia_Production_-_Main/F309370301002204/media/0/0/1ef76b73-3173-43bd-9c16-73986a0ec131/46896726-11e7-4438-b947-d2ae53f58c0b.jpg'
expect(result).toMatchObject([]) })
}) })
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})

View file

@ -40,7 +40,7 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.post(`https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getGridAnon`, null, { .post('https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getGridAnon', null, {
headers: { headers: {
Origin: 'https://www.meo.pt' Origin: 'https://www.meo.pt'
} }

View file

@ -1,101 +1,105 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'meuguia.tv', site: 'meuguia.tv',
days: 2, days: 2,
url({ channel }) { url({ channel }) {
return `https://meuguia.tv/programacao/canal/${channel.site_id}` return `https://meuguia.tv/programacao/canal/${channel.site_id}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
parseItems(content, date).forEach(item => { parseItems(content, date).forEach(item => {
if (dayjs.utc(item.start).isSame(date, 'day')) { if (dayjs.utc(item.start).isSame(date, 'day')) {
programs.push(item) programs.push(item)
} }
}) })
return programs return programs
}, },
async channels() { async channels() {
const channels = [] const channels = []
const axios = require('axios') const axios = require('axios')
const baseUrl = 'https://meuguia.tv' const baseUrl = 'https://meuguia.tv'
let seq = 0 let seq = 0
const queues = [baseUrl] const queues = [baseUrl]
while (true) { while (true) {
if (!queues.length) { if (!queues.length) {
break break
} }
const url = queues.shift() const url = queues.shift()
const content = await axios const content = await axios
.get(url) .get(url)
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
if (content) { if (content) {
const [ $, items ] = getItems(content) const [$, items] = getItems(content)
if (seq === 0) { if (seq === 0) {
queues.push(...items.map(category => baseUrl + $(category).attr('href'))) queues.push(...items.map(category => baseUrl + $(category).attr('href')))
} else { } else {
items.forEach(item => { items.forEach(item => {
const href = $(item).attr('href') const href = $(item).attr('href')
channels.push({ channels.push({
lang: 'pt', lang: 'pt',
site_id: href.substr(href.lastIndexOf('/') + 1), site_id: href.substr(href.lastIndexOf('/') + 1),
name: $(item).find('.licontent h2').text().trim() name: $(item).find('.licontent h2').text().trim()
}) })
}) })
} }
} }
seq++ seq++
} }
return channels return channels
} }
} }
function getItems(content) { function getItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return [$, $('div.mw ul li a').toArray()] return [$, $('div.mw ul li a').toArray()]
} }
function parseItems(content, date) { function parseItems(content, date) {
const result = [] const result = []
const $ = cheerio.load(content) const $ = cheerio.load(content)
let lastDate let lastDate
for (const item of $('ul.mw li').toArray()) { for (const item of $('ul.mw li').toArray()) {
const $item = $(item) const $item = $(item)
if ($item.hasClass('subheader')) { if ($item.hasClass('subheader')) {
lastDate = `${$item.text().split(', ')[1]}/${date.format('YYYY')}` lastDate = `${$item.text().split(', ')[1]}/${date.format('YYYY')}`
} else if ($item.hasClass('divider')) { } else if ($item.hasClass('divider')) {
// ignore // ignore
} else if (lastDate) { } else if (lastDate) {
const data = { title: $item.find('a').attr('title').trim() } const data = { title: $item.find('a').attr('title').trim() }
const ep = data.title.match(/T(\d+) EP(\d+)/) const ep = data.title.match(/T(\d+) EP(\d+)/)
if (ep) { if (ep) {
data.season = parseInt(ep[1]) data.season = parseInt(ep[1])
data.episode = parseInt(ep[2]) data.episode = parseInt(ep[2])
} }
data.start = dayjs.tz(`${lastDate} ${$item.find('.time').text()}`, 'DD/MM/YYYY HH:mm', 'America/Sao_Paulo') data.start = dayjs.tz(
result.push(data) `${lastDate} ${$item.find('.time').text()}`,
} 'DD/MM/YYYY HH:mm',
} 'America/Sao_Paulo'
// use stop time from next item )
if (result.length > 1) { result.push(data)
for (let i = 0; i < result.length - 1; i++) { }
result[i].stop = result[i + 1].start }
} // use stop time from next item
} if (result.length > 1) {
for (let i = 0; i < result.length - 1; i++) {
return result result[i].stop = result[i + 1].start
} }
}
return result
}

View file

@ -1,60 +1,60 @@
const { parser, url } = require('./meuguia.tv.config.js') const { parser, url } = require('./meuguia.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-11-21').startOf('d') const date = dayjs.utc('2023-11-21').startOf('d')
const channel = { const channel = {
site_id: 'AXN', site_id: 'AXN',
xmltv_id: 'AXN.id' xmltv_id: 'AXN.id'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://meuguia.tv/programacao/canal/AXN') expect(url({ channel })).toBe('https://meuguia.tv/programacao/canal/AXN')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = parser({ content, channel, date }).map(p => { const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
if (p.stop) { if (p.stop) {
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
} }
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'Hawaii Five-0 : T10 EP4 - Tiny Is the Flower, Yet It Scents the Grasses Around It', title: 'Hawaii Five-0 : T10 EP4 - Tiny Is the Flower, Yet It Scents the Grasses Around It',
start: '2023-11-21T21:20:00.000Z', start: '2023-11-21T21:20:00.000Z',
stop: '2023-11-21T22:15:00.000Z', stop: '2023-11-21T22:15:00.000Z',
season: 10, season: 10,
episode: 4 episode: 4
}, },
{ {
title: title:
"Hawaii Five-0 : T10 EP5 - Don't Blame Ghosts and Spirits for One's Troubles; A Human Is Responsible", "Hawaii Five-0 : T10 EP5 - Don't Blame Ghosts and Spirits for One's Troubles; A Human Is Responsible",
start: '2023-11-21T22:15:00.000Z', start: '2023-11-21T22:15:00.000Z',
stop: '2023-11-21T23:10:00.000Z', stop: '2023-11-21T23:10:00.000Z',
season: 10, season: 10,
episode: 5 episode: 5
}, },
{ {
title: 'NCIS : T5 EP15 - In the Zone', title: 'NCIS : T5 EP15 - In the Zone',
start: '2023-11-21T23:10:00.000Z', start: '2023-11-21T23:10:00.000Z',
season: 5, season: 5,
episode: 15 episode: 15
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>' content: '<!DOCTYPE html><html><head></head><body></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -42,7 +42,7 @@ module.exports = {
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://www.mewatch.sg/channel-guide`) .get('https://www.mewatch.sg/channel-guide')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -11,9 +11,7 @@ dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
doFetch doFetch.setCheckResult(false).setDebugger(debug)
.setCheckResult(false)
.setDebugger(debug)
const languages = { en: 'english', id: 'indonesia' } const languages = { en: 'english', id: 'indonesia' }
const cookies = {} const cookies = {}
@ -125,7 +123,7 @@ async function parseItems(content, date, cookies) {
const url = $item.find('a').attr('href') const url = $item.find('a').attr('href')
const headers = { const headers = {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
Cookie: cookies, Cookie: cookies
} }
queues.push({ i: $item, url, params: { headers, timeout } }) queues.push({ i: $item, url, params: { headers, timeout } })
} }

View file

@ -48,8 +48,10 @@ function parseItems(context) {
schDayPrograms.forEach((program, i) => { schDayPrograms.forEach((program, i) => {
const itemDay = { const itemDay = {
progStart: parseStart($(schDayMonth), $(program)), progStart: parseStart($(schDayMonth), $(program)),
progStop: parseStop($(schDayMonth), schDayPrograms[i + 1] ? progStop: parseStop(
$(schDayPrograms[i + 1]) : null), $(schDayMonth),
schDayPrograms[i + 1] ? $(schDayPrograms[i + 1]) : null
),
progTitle: parseTitle($(program)), progTitle: parseTitle($(program)),
progDesc: parseDescription($(program)) progDesc: parseDescription($(program))
} }
@ -91,7 +93,9 @@ function parseStop(schDayMonth, itemNext) {
) )
} else { } else {
return dayjs.tz( return dayjs.tz(
`${currentYear}-${monthDate[0]}-${(parseInt(monthDate[1]) + 1).toString().padStart(2, '0')} 00:00`, `${currentYear}-${monthDate[0]}-${(parseInt(monthDate[1]) + 1)
.toString()
.padStart(2, '0')} 00:00`,
'YYYY-MMM-DD HH:mm', 'YYYY-MMM-DD HH:mm',
tz tz
) )

File diff suppressed because one or more lines are too long

View file

@ -41,7 +41,7 @@ module.exports = {
const pages = Array.from(Array(totalPages).keys()) const pages = Array.from(Array(totalPages).keys())
for (let page of pages) { for (let page of pages) {
const data = await axios const data = await axios
.get(`https://mtel.ba/oec/epg/program`, { .get('https://mtel.ba/oec/epg/program', {
params: { page, date: dayjs().format('YYYY-MM-DD') }, params: { page, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@ -65,7 +65,7 @@ module.exports = {
async function getTotalPageCount() { async function getTotalPageCount() {
const data = await axios const data = await axios
.get(`https://mtel.ba/oec/epg/program`, { .get('https://mtel.ba/oec/epg/program', {
params: { page: 0, date: dayjs().format('YYYY-MM-DD') }, params: { page: 0, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'

View file

@ -43,7 +43,7 @@ module.exports = {
const pages = Array.from(Array(totalPages).keys()) const pages = Array.from(Array(totalPages).keys())
for (let page of pages) { for (let page of pages) {
const data = await axios const data = await axios
.get(`https://mts.rs/oec/epg/program`, { .get('https://mts.rs/oec/epg/program', {
params: { page, date: dayjs().format('YYYY-MM-DD') }, params: { page, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@ -67,7 +67,7 @@ module.exports = {
async function getTotalPageCount() { async function getTotalPageCount() {
const data = await axios const data = await axios
.get(`https://mts.rs/oec/epg/program`, { .get('https://mts.rs/oec/epg/program', {
params: { page: 0, date: dayjs().format('YYYY-MM-DD') }, params: { page: 0, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@ -84,8 +84,8 @@ function parseContent(content, channel) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
console.log(error) return []
} }
if (!data || !data.channels || !data.channels.length) return null if (!data || !data.channels || !data.channels.length) return null

View file

@ -47,7 +47,7 @@ module.exports = {
const data = await axios const data = await axios
.post( .post(
`https://services.mujtvprogram.cz/tvprogram2services/services/tvchannellist_mobile.php`, 'https://services.mujtvprogram.cz/tvprogram2services/services/tvchannellist_mobile.php',
params, params,
{ {
headers: { headers: {
@ -86,7 +86,7 @@ function parseItems(content) {
if (!data) return [] if (!data) return []
const programmes = data['tv-program-programmes'].programme const programmes = data['tv-program-programmes'].programme
return programmes && Array.isArray(programmes) ? programmes : [] return programmes && Array.isArray(programmes) ? programmes : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -9,7 +9,7 @@ dayjs.extend(customParseFormat)
const headers = { const headers = {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0'
} }
module.exports = { module.exports = {

View file

@ -26,12 +26,11 @@ it('can generate valid url for today', () => {
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }) const results = parser({ content, date }).map(p => {
.map(p => { p.start = p.start.toJSON()
p.start = p.start.toJSON() p.stop = p.stop.toJSON()
p.stop = p.stop.toJSON() return p
return p })
})
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-19T23:00:00.000Z', start: '2022-11-19T23:00:00.000Z',

View file

@ -23,14 +23,15 @@ module.exports = {
channel.site_id channel.site_id
}.html?dt=${date.format('YYYY-MM-DD')}` }.html?dt=${date.format('YYYY-MM-DD')}`
}, },
async parser({ content, date, channel }) { async parser({ content, date }) {
const programs = [] const programs = []
if (content) { if (content) {
const queues = [] const queues = []
const $ = cheerio.load(content) const $ = cheerio.load(content)
$('table.table > tbody > tr').toArray() $('table.table > tbody > tr')
.toArray()
.forEach(el => { .forEach(el => {
const td = $(el).find('td:eq(1)') const td = $(el).find('td:eq(1)')
const title = td.find('h5 a') const title = td.find('h5 a')
@ -66,12 +67,16 @@ module.exports = {
const subTitle = parseText($('.tab-pane > h5 > strong')) const subTitle = parseText($('.tab-pane > h5 > strong'))
const description = parseText($('.tab-pane > .tvbody > p')) const description = parseText($('.tab-pane > .tvbody > p'))
const image = $('.program-media-image img').attr('src') const image = $('.program-media-image img').attr('src')
const category = $('.schedule-attributes-genres span').toArray() const category = $('.schedule-attributes-genres span')
.toArray()
.map(el => $(el).text()) .map(el => $(el).text())
const casts = $('.single-cast-head:not([id])').toArray() const casts = $('.single-cast-head:not([id])')
.toArray()
.map(el => { .map(el => {
const cast = { name: parseText($(el).find('a')) } const cast = { name: parseText($(el).find('a')) }
const [, role] = $(el).text().match(/\((.*)\)/) || [null, null] const [, role] = $(el)
.text()
.match(/\((.*)\)/) || [null, null]
if (role) { if (role) {
cast.role = role cast.role = role
} }
@ -102,7 +107,7 @@ module.exports = {
start, start,
stop stop
}) })
}) })
} }
} }
@ -115,11 +120,17 @@ module.exports = {
// process form -> provider // process form -> provider
if (queue.t === 'p') { if (queue.t === 'p') {
const $ = cheerio.load(res) const $ = cheerio.load(res)
$('#guide_provider option').toArray() $('#guide_provider option')
.toArray()
.forEach(el => { .forEach(el => {
const opt = $(el) const opt = $(el)
const provider = opt.attr('value') const provider = opt.attr('value')
queues.push({ t: 'r', method: 'post', url: 'https://www.mytelly.co.uk/getregions', params: { provider } }) queues.push({
t: 'r',
method: 'post',
url: 'https://www.mytelly.co.uk/getregions',
params: { provider }
})
}) })
} }
// process provider -> region // process provider -> region
@ -135,26 +146,30 @@ module.exports = {
u_time: now.format('HHmm'), u_time: now.format('HHmm'),
is_mobile: 1 is_mobile: 1
} }
queues.push({ t: 's', method: 'post', url: 'https://www.mytelly.co.uk/tv-guide/schedule', params }) queues.push({
t: 's',
method: 'post',
url: 'https://www.mytelly.co.uk/tv-guide/schedule',
params
})
} }
} }
// process schedule -> channels // process schedule -> channels
if (queue.t === 's') { if (queue.t === 's') {
const $ = cheerio.load(res) const $ = cheerio.load(res)
$('.channelname') $('.channelname').each((i, el) => {
.each((i, el) => { const name = $(el).find('center > a:eq(1)').text()
const name = $(el).find('center > a:eq(1)').text() const url = $(el).find('center > a:eq(1)').attr('href')
const url = $(el).find('center > a:eq(1)').attr('href') const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) const site_id = `${number}/${slug}`
const site_id = `${number}/${slug}` if (channels[site_id] === undefined) {
if (channels[site_id] === undefined) { channels[site_id] = {
channels[site_id] = { lang: 'en',
lang: 'en', site_id,
site_id, name
name
}
} }
}) }
})
} }
}) })
@ -178,13 +193,10 @@ function parseTime(date, time) {
} }
function parseText($item) { function parseText($item) {
let text = $item.text() let text = $item.text().replace(/\t/g, '').replace(/\n/g, ' ').trim()
.replace(/\t/g, '')
.replace(/\n/g, ' ')
.trim()
while (true) { while (true) {
if (text.match(/ /)) { if (text.match(/\s\s/)) {
text = text.replace(/ /g, ' ') text = text.replace(/\s\s/g, ' ')
continue continue
} }
break break

View file

@ -17,16 +17,18 @@ const channel = {
xmltv_id: 'BBCOneLondon.uk' xmltv_id: 'BBCOneLondon.uk'
} }
axios.get.mockImplementation((url, opts) => { axios.get.mockImplementation(url => {
if ( if (
url === 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=1906433&tm=2024-12-07+00%3A00%3A00' url ===
'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=1906433&tm=2024-12-07+00%3A00%3A00'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html')) data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html'))
}) })
} }
if ( if (
url === 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=5656624&tm=2024-12-07+23%3A35%3A00' url ===
'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=5656624&tm=2024-12-07+23%3A35%3A00'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html')) data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html'))
@ -57,7 +59,8 @@ it('can parse response', async () => {
title: 'Captain Phillips', title: 'Captain Phillips',
description: description:
'An American cargo ship sets a dangerous course around the coast of Somalia, while inland, four men are pressed into service as pirates by the local warlords. The captain is taken hostage when the raiding party hijacks the vessel, resulting in a tense five-day crisis. Fact-based thriller, starring Tom Hanks and Barkhad Abdi', 'An American cargo ship sets a dangerous course around the coast of Somalia, while inland, four men are pressed into service as pirates by the local warlords. The captain is taken hostage when the raiding party hijacks the vessel, resulting in a tense five-day crisis. Fact-based thriller, starring Tom Hanks and Barkhad Abdi',
image: 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/c44ce7b0d3ae602c0c93ece5af140815.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4dsylOCGGE7OWlqwSWt0cd0Qtrin4DkEMC0Zzdp8ZeNk2vNIQzjMF0DG0h3IeTR5NM%3D', image:
'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/c44ce7b0d3ae602c0c93ece5af140815.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4dsylOCGGE7OWlqwSWt0cd0Qtrin4DkEMC0Zzdp8ZeNk2vNIQzjMF0DG0h3IeTR5NM%3D',
category: ['Factual', 'Movie/Drama', 'Thriller'] category: ['Factual', 'Movie/Drama', 'Thriller']
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
@ -67,7 +70,8 @@ it('can parse response', async () => {
subTitle: 'Past and Pressure Season 6, Episode 5', subTitle: 'Past and Pressure Season 6, Episode 5',
description: description:
'The artists are tasked with writing a song about their heritage. For some, the pressure of the competition proves too much for them to match. In their final challenge, they are put face to face with industry experts who grill them about their plans after the competition. Some impress, while others leave the mentors confused', 'The artists are tasked with writing a song about their heritage. For some, the pressure of the competition proves too much for them to match. In their final challenge, they are put face to face with industry experts who grill them about their plans after the competition. Some impress, while others leave the mentors confused',
image: 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/2039278182b27cc279570b9ab9b89379.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4cDhR7jXTNFW3tgwQCdOPUobhXwlT81mIsqOe93HPusDG6tw1aoeYOgafojtynNWxc%3D', image:
'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/2039278182b27cc279570b9ab9b89379.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4cDhR7jXTNFW3tgwQCdOPUobhXwlT81mIsqOe93HPusDG6tw1aoeYOgafojtynNWxc%3D',
category: ['Challenge/Reality Show', 'Show/Game Show'], category: ['Challenge/Reality Show', 'Show/Game Show'],
season: 6, season: 6,
episode: 5 episode: 5

View file

@ -1,73 +1,80 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'neo.io', site: 'neo.io',
timezone: 'Europe/Ljubljana', timezone: 'Europe/Ljubljana',
days: 5, days: 5,
url({ date, channel }) { return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' }, url() {
request: { return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
method: 'POST', },
headers: { request: {
'Host': 'stargate.telekom.si', method: 'POST',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', headers: {
'Accept': 'application/json, text/plain, */*', Host: 'stargate.telekom.si',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', 'User-Agent':
'Content-Type': 'application/json', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'X-AppLayout': '1', Accept: 'application/json, text/plain, */*',
'x-language': 'sl', 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Origin': 'https://neo.io', 'Content-Type': 'application/json',
'Sec-Fetch-Dest': 'empty', 'X-AppLayout': '1',
'Sec-Fetch-Mode': 'cors', 'x-language': 'sl',
'Sec-Fetch-Site': 'cross-site', Origin: 'https://neo.io',
'Sec-GPC': '1', 'Sec-Fetch-Dest': 'empty',
'Connection': 'keep-alive' 'Sec-Fetch-Mode': 'cors',
}, 'Sec-Fetch-Site': 'cross-site',
data({ channel, date }) { 'Sec-GPC': '1',
const todayEpoch = date.startOf('day').unix(); Connection: 'keep-alive'
const nextDayEpoch = date.add(1, 'day').startOf('day').unix(); },
return JSON.stringify({ data({ channel, date }) {
ch_ext_id: channel.site_id, const todayEpoch = date.startOf('day').unix()
from: todayEpoch, const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
to: nextDayEpoch return JSON.stringify({
}) ch_ext_id: channel.site_id,
} from: todayEpoch,
}, to: nextDayEpoch
parser: function ({ content }) { })
const programs = []; }
const data = JSON.parse(content); },
data.shows.forEach(show => { parser: function ({ content }) {
const start = dayjs.unix(show.show_start).utc(); const programs = []
const stop = dayjs.unix(show.show_end).utc(); const data = JSON.parse(content)
const programData = { data.shows.forEach(show => {
title: show.title, const start = dayjs.unix(show.show_start).utc()
description: show.summary || 'No description available', const stop = dayjs.unix(show.show_end).utc()
start: start.toISOString(), const programData = {
stop: stop.toISOString(), title: show.title,
thumbnail: show.thumbnail description: show.summary || 'No description available',
} start: start.toISOString(),
programs.push(programData) stop: stop.toISOString(),
}) thumbnail: show.thumbnail
return programs }
}, programs.push(programData)
async channels() { })
const response = await axios.post('https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList', JSON.stringify({ includeRadioStations: true }), { return programs
headers: this.request.headers },
}); async channels() {
const response = await axios.post(
const data = response.data.data; 'https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList',
return data.map(item => ({ JSON.stringify({ includeRadioStations: true }),
lang: 'sq', {
name: String(item.channel.title), headers: this.request.headers
site_id: String(item.channel.id), }
//logo: String(item.channel.logo) )
}))
} const data = response.data.data
} return data.map(item => ({
lang: 'sq',
name: String(item.channel.title),
site_id: String(item.channel.id)
//logo: String(item.channel.logo)
}))
}
}

View file

@ -1,121 +1,124 @@
const { parser, url } = require('./neo.io.config.js') const { parser, url } = require('./neo.io.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('day') const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('day')
const channel = { const channel = {
site_id: 'tv-slo-1', site_id: 'tv-slo-1',
xmltv_id: 'TVSLO1.si' xmltv_id: 'TVSLO1.si'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData') expect(url({ date, channel })).toBe(
}) 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
)
it('can parse response', () => { })
const content = `
{ it('can parse response', () => {
"shows": [ const content = `
{ {
"title": "Napovedujemo", "shows": [
"show_start": 1735185900, {
"show_end": 1735192200, "title": "Napovedujemo",
"timestamp": "5:05 - 6:50", "show_start": 1735185900,
"show_id": "CUP_IECOM_SLO1_10004660", "show_end": 1735192200,
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg", "timestamp": "5:05 - 6:50",
"is_adult": false, "show_id": "CUP_IECOM_SLO1_10004660",
"friendly_id": "napovedujemo_db48", "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg",
"pg": "", "is_adult": false,
"genres": [ "friendly_id": "napovedujemo_db48",
"napovednik" "pg": "",
], "genres": [
"year": 0, "napovednik"
"summary": "Vabilo k ogledu naših oddaj.", ],
"categories": "Ostalo", "year": 0,
"stb_only": false, "summary": "Vabilo k ogledu naših oddaj.",
"is_live": false, "categories": "Ostalo",
"original_title": "Napovedujemo" "stb_only": false,
}, "is_live": false,
{ "original_title": "Napovedujemo"
"title": "S0E0 - Hrabri zajčki: Prvi sneg", },
"show_start": 1735192200, {
"show_end": 1735192800, "title": "S0E0 - Hrabri zajčki: Prvi sneg",
"timestamp": "6:50 - 7:00", "show_start": 1735192200,
"show_id": "CUP_IECOM_SLO1_79637910", "show_end": 1735192800,
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg", "timestamp": "6:50 - 7:00",
"is_adult": false, "show_id": "CUP_IECOM_SLO1_79637910",
"friendly_id": "hrabri_zajcki_prvi_sneg_1619", "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg",
"pg": "", "is_adult": false,
"genres": [ "friendly_id": "hrabri_zajcki_prvi_sneg_1619",
"risanka" "pg": "",
], "genres": [
"year": 2020, "risanka"
"summary": "Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.", ],
"categories": "Otroški/Mladinski", "year": 2020,
"stb_only": false, "summary": "Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.",
"is_live": false, "categories": "Otroški/Mladinski",
"original_title": "S0E0 - Brave Bunnies" "stb_only": false,
}, "is_live": false,
{ "original_title": "S0E0 - Brave Bunnies"
"title": "Dobro jutro", },
"show_start": 1735192800, {
"show_end": 1735203900, "title": "Dobro jutro",
"timestamp": "7:00 - 10:05", "show_start": 1735192800,
"show_id": "CUP_IECOM_SLO1_79637911", "show_end": 1735203900,
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg", "timestamp": "7:00 - 10:05",
"is_adult": false, "show_id": "CUP_IECOM_SLO1_79637911",
"friendly_id": "dobro_jutro_2f10", "thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg",
"pg": "", "is_adult": false,
"genres": [ "friendly_id": "dobro_jutro_2f10",
"zabavna oddaja" "pg": "",
], "genres": [
"year": 2024, "zabavna oddaja"
"summary": "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.", ],
"categories": "Razvedrilni program", "year": 2024,
"stb_only": false, "summary": "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.",
"is_live": false, "categories": "Razvedrilni program",
"original_title": "Dobro jutro" "stb_only": false,
} "is_live": false,
] "original_title": "Dobro jutro"
}` }
]
const result = parser({ content, channel }).map(p => { }`
p.start = p.start
p.stop = p.stop const result = parser({ content, channel })
return p
}) expect(result).toMatchObject([
{
expect(result).toMatchObject([ title: 'Napovedujemo',
{ description: 'Vabilo k ogledu naših oddaj.',
title: "Napovedujemo", start: '2024-12-26T04:05:00.000Z',
description: "Vabilo k ogledu naših oddaj.", stop: '2024-12-26T05:50:00.000Z',
start: "2024-12-26T04:05:00.000Z", thumbnail:
stop: "2024-12-26T05:50:00.000Z", 'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg'
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg" },
}, {
{ title: 'S0E0 - Hrabri zajčki: Prvi sneg',
title: "S0E0 - Hrabri zajčki: Prvi sneg", description:
description: "Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.", 'Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.',
start: "2024-12-26T05:50:00.000Z", start: '2024-12-26T05:50:00.000Z',
stop: "2024-12-26T06:00:00.000Z", stop: '2024-12-26T06:00:00.000Z',
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg" thumbnail:
}, 'https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg'
{ },
title: "Dobro jutro", {
description: "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.", title: 'Dobro jutro',
start: "2024-12-26T06:00:00.000Z", description:
stop: "2024-12-26T09:05:00.000Z", 'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.',
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg" start: '2024-12-26T06:00:00.000Z',
} stop: '2024-12-26T09:05:00.000Z',
]) thumbnail:
}) 'https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg'
}
it('can handle empty guide', () => { ])
const result = parser({ })
content: '{"shows":[]}'
}) it('can handle empty guide', () => {
expect(result).toMatchObject([]) const result = parser({
}) content: '{"shows":[]}'
})
expect(result).toMatchObject([])
})

View file

@ -50,7 +50,7 @@ function parseItems(content, date) {
if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] if (!data || !data.item || !Array.isArray(data.item.episodes)) return []
return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD')))
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -1,45 +1,46 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'nhl.com', site: 'nhl.com',
// I'm not sure what `endDate` represents but they only return 1 day of // I'm not sure what `endDate` represents but they only return 1 day of
// results, with `endTime`s ocassionally in the following day. // results, with `endTime`s ocassionally in the following day.
days: 1, days: 1,
url: ({ date }) => `https://api-web.nhle.com/v1/network/tv-schedule/${date.toJSON().split("T")[0]}`, url: ({ date }) =>
parser({ content }) { `https://api-web.nhle.com/v1/network/tv-schedule/${date.toJSON().split('T')[0]}`,
const programs = [] parser({ content }) {
const items = parseItems(content) const programs = []
for (const item of items) { const items = parseItems(content)
programs.push({ for (const item of items) {
title: item.title, programs.push({
description: item.description === item.title ? undefined : item.description, title: item.title,
category: "Sports", description: item.description === item.title ? undefined : item.description,
// image: parseImage(item), category: 'Sports',
start: parseStart(item), // image: parseImage(item),
stop: parseStop(item) start: parseStart(item),
}) stop: parseStop(item)
} })
}
return programs
} return programs
} }
}
// Unfortunately I couldn't determine how these are
// supposed to be formatted. Pointers appreciated! // Unfortunately I couldn't determine how these are
// function parseImage(item) { // supposed to be formatted. Pointers appreciated!
// const uri = item.broadcastImageUrl // function parseImage(item) {
// const uri = item.broadcastImageUrl
// return uri ? `https://???/${uri}` : null
// } // return uri ? `https://???/${uri}` : null
// }
function parseStart(item) {
return dayjs(item.startTime) function parseStart(item) {
} return dayjs(item.startTime)
}
function parseStop(item) {
return dayjs(item.endTime) function parseStop(item) {
} return dayjs(item.endTime)
}
function parseItems(content) {
return JSON.parse(content).broadcasts function parseItems(content) {
} return JSON.parse(content).broadcasts
}

View file

@ -1,44 +1,44 @@
const { parser, url } = require('./nhl.com.config.js') const { parser, url } = require('./nhl.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2024-11-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-11-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe( expect(url({ date })).toBe('https://api-web.nhle.com/v1/network/tv-schedule/2024-11-21')
'https://api-web.nhle.com/v1/network/tv-schedule/2024-11-21' })
)
}) it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
it('can parse response', () => { let results = parser({ content, date })
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) results = results.map(p => {
let results = parser({ content, date }) p.start = p.start.toJSON()
results = results.map(p => { p.stop = p.stop.toJSON()
p.start = p.start.toJSON() return p
p.stop = p.stop.toJSON() })
return p
}) expect(results[0]).toMatchObject({
start: '2024-11-21T12:00:00.000Z',
expect(results[0]).toMatchObject({ stop: '2024-11-21T13:00:00.000Z',
start: '2024-11-21T12:00:00.000Z', title: 'On The Fly',
stop: '2024-11-21T13:00:00.000Z', category: 'Sports'
title: 'On The Fly', })
category: 'Sports', })
})
}) it('can handle empty guide', () => {
const results = parser({
it('can handle empty guide', () => { content: JSON.stringify({
const results = parser({ content: JSON.stringify({ // extra props not necessary but they form a valid response
// extra props not necessary but they form a valid response date: '2024-11-21',
date: "2024-11-21", startDate: '2024-11-07',
startDate: "2024-11-07", endDate: '2024-12-05',
endDate: "2024-12-05", broadcasts: []
broadcasts: [], })
}) }) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

@ -1,68 +1,68 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const headers = { const headers = {
'X-Apikey': 'xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI', 'X-Apikey': 'xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI',
'X-Core-Appversion': '2.14.0.1', 'X-Core-Appversion': '2.14.0.1',
'X-Core-Contentratinglimit': '0', 'X-Core-Contentratinglimit': '0',
'X-Core-Deviceid': '', 'X-Core-Deviceid': '',
'X-Core-Devicetype': 'web', 'X-Core-Devicetype': 'web',
Origin: 'https://nostv.pt', Origin: 'https://nostv.pt',
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
} }
module.exports = { module.exports = {
site: 'nostv.pt', site: 'nostv.pt',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://tyr-prod.apigee.net/nostv/ott/schedule/range/contents/guest?channels=${ return `https://tyr-prod.apigee.net/nostv/ott/schedule/range/contents/guest?channels=${
channel.site_id channel.site_id
}&minDate=${date.format('YYYY-MM-DD')}T00:00:00Z&maxDate=${date.format( }&minDate=${date.format('YYYY-MM-DD')}T00:00:00Z&maxDate=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}T23:59:59Z&isDateInclusive=true&client_id=${headers['X-Apikey']}` )}T23:59:59Z&isDateInclusive=true&client_id=${headers['X-Apikey']}`
}, },
request: { headers }, request: { headers },
parser({ content }) { parser({ content }) {
const programs = [] const programs = []
if (content) { if (content) {
const items = Array.isArray(content) ? content : JSON.parse(content) const items = Array.isArray(content) ? content : JSON.parse(content)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.Metadata?.Title, title: item.Metadata?.Title,
sub_title: item.Metadata?.SubTitle ? item.Metadata?.SubTitle : null, sub_title: item.Metadata?.SubTitle ? item.Metadata?.SubTitle : null,
description: item.Metadata?.Description, description: item.Metadata?.Description,
season: item.Metadata?.Season, season: item.Metadata?.Season,
episode: item.Metadata?.Episode, episode: item.Metadata?.Episode,
image: item.Images image: item.Images
? `https://mage.stream.nos.pt/v1/nostv_mage/Images?sourceUri=${item.Images[0].Url}&profile=ott_1_452x340&client_id=${headers['X-Apikey']}` ? `https://mage.stream.nos.pt/v1/nostv_mage/Images?sourceUri=${item.Images[0].Url}&profile=ott_1_452x340&client_id=${headers['X-Apikey']}`
: null, : null,
start: dayjs.utc(item.UtcDateTimeStart), start: dayjs.utc(item.UtcDateTimeStart),
stop: dayjs.utc(item.UtcDateTimeEnd) stop: dayjs.utc(item.UtcDateTimeEnd)
}) })
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const result = await axios const result = await axios
.get( .get(
`https://tyr-prod.apigee.net/nostv/ott/channels/guest?client_id=${headers['X-Apikey']}`, `https://tyr-prod.apigee.net/nostv/ott/channels/guest?client_id=${headers['X-Apikey']}`,
{ headers } { headers }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return result.map(item => { return result.map(item => {
return { return {
lang: 'pt', lang: 'pt',
site_id: item.ServiceId, site_id: item.ServiceId,
name: item.Name name: item.Name
} }
}) })
} }
} }

View file

@ -1,51 +1,51 @@
const { parser, url } = require('./nostv.pt.config.js') const { parser, url } = require('./nostv.pt.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-12-11').startOf('d') const date = dayjs.utc('2023-12-11').startOf('d')
const channel = { const channel = {
site_id: '510', site_id: '510',
xmltv_id: 'SPlus.pt' xmltv_id: 'SPlus.pt'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://tyr-prod.apigee.net/nostv/ott/schedule/range/contents/guest?channels=510&minDate=2023-12-11T00:00:00Z&maxDate=2023-12-11T23:59:59Z&isDateInclusive=true&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI' 'https://tyr-prod.apigee.net/nostv/ott/schedule/range/contents/guest?channels=510&minDate=2023-12-11T00:00:00Z&maxDate=2023-12-11T23:59:59Z&isDateInclusive=true&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
const results = parser({ content }).map(p => { const results = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-12-11T16:30:00.000Z', start: '2023-12-11T16:30:00.000Z',
stop: '2023-12-11T17:00:00.000Z', stop: '2023-12-11T17:00:00.000Z',
title: 'Village Vets', title: 'Village Vets',
description: description:
'A história de dois melhores amigos veterinários e o seu extraordinário trabalho na Austrália.', 'A história de dois melhores amigos veterinários e o seu extraordinário trabalho na Austrália.',
season: 1, season: 1,
episode: 12, episode: 12,
image: image:
'https://mage.stream.nos.pt/v1/nostv_mage/Images?sourceUri=http://vip.pam.local.internal/PAM.Images/Store/8329ed1aec5d4c0faa2056972256ff9f&profile=ott_1_452x340&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI' 'https://mage.stream.nos.pt/v1/nostv_mage/Images?sourceUri=http://vip.pam.local.internal/PAM.Images/Store/8329ed1aec5d4c0faa2056972256ff9f&profile=ott_1_452x340&client_id=xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI'
}) })
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const results = await parser({ const results = await parser({
date, date,
content: '[]' content: '[]'
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

@ -55,7 +55,7 @@ module.exports = {
.map(function () { .map(function () {
return { return {
lang: 'es', lang: 'es',
site_id: $(this).attr('alt').replace(/\&/gi, '&amp;'), site_id: $(this).attr('alt').replace(/&/gi, '&amp;'),
name: $(this).attr('alt') name: $(this).attr('alt')
} }
}) })

View file

@ -1,81 +1,81 @@
const parser = require('epg-parser') const parser = require('epg-parser')
module.exports = { module.exports = {
site: 'nzxmltv.com', site: 'nzxmltv.com',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 3600000 // 1 hour ttl: 3600000 // 1 hour
}, },
maxContentLength: 104857600 // 100 MB maxContentLength: 104857600 // 100 MB
}, },
url({ channel }) { url({ channel }) {
const [path] = channel.site_id.split('#') const [path] = channel.site_id.split('#')
return `https://nzxmltv.com/${path}.xml` return `https://nzxmltv.com/${path}.xml`
}, },
parser({ content, channel, date }) { parser({ content, channel, date }) {
const programs = [] const programs = []
parseItems(content, channel, date).forEach(item => { parseItems(content, channel, date).forEach(item => {
const program = { const program = {
title: item.title?.[0]?.value, title: item.title?.[0]?.value,
description: item.desc?.[0]?.value, description: item.desc?.[0]?.value,
icon: item.icon?.[0], icon: item.icon?.[0],
start: item.start, start: item.start,
stop: item.stop stop: item.stop
} }
if (item.episodeNum) { if (item.episodeNum) {
item.episodeNum.forEach(ep => { item.episodeNum.forEach(ep => {
if (ep.system === 'xmltv_ns') { if (ep.system === 'xmltv_ns') {
const [season, episode, _] = ep.value.split('.') const [season, episode] = ep.value.split('.')
program.season = parseInt(season) + 1 program.season = parseInt(season) + 1
program.episode = parseInt(episode) + 1 program.episode = parseInt(episode) + 1
return true return true
} }
}) })
} }
programs.push(program) programs.push(program)
}) })
return programs return programs
}, },
async channels({ provider }) { async channels({ provider }) {
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const providers = { const providers = {
freeview: 'xmltv/guide', freeview: 'xmltv/guide',
sky: 'sky/guide', sky: 'sky/guide',
redbull: 'iptv/redbull', redbull: 'iptv/redbull',
pluto: 'iptv/plutotv' pluto: 'iptv/plutotv'
} }
const channels = [] const channels = []
const path = providers[provider] const path = providers[provider]
const xml = await axios const xml = await axios
.get(`https://nzxmltv.com/${path}.xml`) .get(`https://nzxmltv.com/${path}.xml`)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
const $ = cheerio.load(xml) const $ = cheerio.load(xml)
$('tv channel').each((i, el) => { $('tv channel').each((i, el) => {
const disp = $(el).find('display-name') const disp = $(el).find('display-name')
const channelId = $(el).attr('id') const channelId = $(el).attr('id')
channels.push({ channels.push({
lang: disp.attr('lang').substr(0, 2), lang: disp.attr('lang').substr(0, 2),
site_id: `${path}#${channelId}`, site_id: `${path}#${channelId}`,
name: disp.text().trim() name: disp.text().trim()
}) })
}) })
return channels return channels
} }
} }
function parseItems(content, channel, date) { function parseItems(content, channel, date) {
const { programs } = parser.parse(content) const { programs } = parser.parse(content)
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day')) return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day'))
} }

View file

@ -1,40 +1,40 @@
const { parser, url } = require('./nzxmltv.com.config.js') const { parser, url } = require('./nzxmltv.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-11-21').startOf('d') const date = dayjs.utc('2023-11-21').startOf('d')
const channel = { const channel = {
site_id: 'xmltv/guide#1', site_id: 'xmltv/guide#1',
xmltv_id: 'TVNZ1.nz' xmltv_id: 'TVNZ1.nz'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://nzxmltv.com/xmltv/guide.xml') expect(url({ channel })).toBe('https://nzxmltv.com/xmltv/guide.xml')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
const results = parser({ content, channel, date }) const results = parser({ content, channel, date })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-11-21T10:30:00.000Z', start: '2023-11-21T10:30:00.000Z',
stop: '2023-11-21T11:25:00.000Z', stop: '2023-11-21T11:25:00.000Z',
title: 'Sunday', title: 'Sunday',
description: description:
'On Sunday, an unmissable show with stories about divorce, weight loss, and the incomprehensible devastation of Gaza.', 'On Sunday, an unmissable show with stories about divorce, weight loss, and the incomprehensible devastation of Gaza.',
season: 2023, season: 2023,
episode: 37, episode: 37,
icon: 'https://www.thetvdb.com/banners/posters/5dbebff2986f2.jpg' icon: 'https://www.thetvdb.com/banners/posters/5dbebff2986f2.jpg'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ content: '', channel, date }) const result = parser({ content: '', channel, date })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -132,7 +132,7 @@ module.exports = {
const $ = cheerio.load(data) const $ = cheerio.load(data)
$('.channelname').each((i, el) => { $('.channelname').each((i, el) => {
let name = $(el).find('center > a:eq(1)').text() let name = $(el).find('center > a:eq(1)').text()
name = name.replace(/\-\-/gi, '-') name = name.replace(/--/gi, '-')
const url = $(el).find('center > a:eq(1)').attr('href') const url = $(el).find('center > a:eq(1)').attr('href')
if (!url) return if (!url) return
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)

View file

@ -1,113 +1,108 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const axios = require('axios') const axios = require('axios')
dayjs.extend(utc) dayjs.extend(utc)
const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO' const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO'
const API_CHANNEL_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json' const API_CHANNEL_ENDPOINT =
const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images' 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json'
const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images'
module.exports = {
site: 'orangetv.orange.es', module.exports = {
days: 2, site: 'orangetv.orange.es',
request: { days: 2,
cache: { request: {
ttl: 60 * 60 * 1000 // 1 hour cache: {
} ttl: 60 * 60 * 1000 // 1 hour
}, }
url({ date }) { },
return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json` url({ date }) {
}, return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`
async parser({ content, channel, date }) { },
let programs = [] async parser({ content, channel, date }) {
let items = parseItems(content, channel) let programs = []
if (!items.length) return programs let items = parseItems(content, channel)
if (!items.length) return programs
const promises = [
axios.get( const promises = [
`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`, axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`),
), axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_2.json`),
axios.get( axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_3.json`)
`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_2.json`, ]
),
axios.get( await Promise.allSettled(promises)
`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_3.json`, .then(results => {
), results.forEach(r => {
] if (r.status === 'fulfilled') {
const parsed = parseItems(r.value.data, channel)
await Promise.allSettled(promises)
.then(results => { items = items
results.forEach(r => { .filter((item, index) => items.findIndex(oi => oi.id === item.id) === index)
if (r.status === 'fulfilled') { .concat(parsed)
const parsed = parseItems(r.value.data, channel) }
})
items = items.filter((item, index) => items.findIndex(oi => oi.id === item.id) === index).concat(parsed) })
} .catch(console.error)
})
}) items.forEach(item => {
.catch(console.error) programs.push({
title: item.name,
items.forEach(item => { description: item.description,
programs.push({ category: parseGenres(item),
title: item.name, season: item.seriesSeason || null,
description: item.description, episode: item.episodeId || null,
category: parseGenres(item), icon: parseIcon(item),
season: item.seriesSeason || null, start: dayjs.utc(item.startDate) || null,
episode: item.episodeId || null, stop: dayjs.utc(item.endDate) || null
icon: parseIcon(item), })
start: dayjs.utc(item.startDate) || null, })
stop: dayjs.utc(item.endDate) || null,
}) return programs
}) },
async channels() {
return programs const axios = require('axios')
}, const data = await axios
async channels() { .get(API_CHANNEL_ENDPOINT)
const axios = require('axios') .then(r => r.data)
const data = await axios .catch(console.log)
.get(API_CHANNEL_ENDPOINT) return data.response.map(item => {
.then(r => r.data) return {
.catch(console.log) lang: 'es',
return data.response.map(item => { name: item.name,
return { site_id: item.externalChannelId
lang: 'es', }
name: item.name, })
site_id: item.externalChannelId }
} }
})
} function parseIcon(item) {
} if (item.attachments.length > 0) {
const cover = item.attachments.find(i => i.name === 'COVER' || i.name === 'cover')
function parseIcon(item){
if (cover) {
if(item.attachments.length > 0){ return `${API_IMAGE_ENDPOINT}${cover.value}`
const cover = item.attachments.find(i => i.name === "COVER" || i.name === "cover") }
}
if(cover)
{ return ''
return `${API_IMAGE_ENDPOINT}${cover.value}`; }
}
} function parseGenres(item) {
return item.genres.map(i => i.name)
return '' }
}
function parseItems(content, channel) {
function parseGenres(item){ const json =
return item.genres.map(i => i.name); typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : []
}
if (!Array.isArray(json)) {
function parseItems(content, channel) { return []
const json = typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : [] }
if (!Array.isArray(json)) { const channelData = json.find(i => i.channelExternalId == channel.site_id)
return [];
} if (!channelData) return []
const channelData = json.find(i => i.channelExternalId == channel.site_id); return channelData.programs
}
if(!channelData)
return [];
return channelData.programs;
}

View file

@ -1,49 +1,54 @@
const { parser, url } = require('./orangetv.orange.es.config.js') const { parser, url } = require('./orangetv.orange.es.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const date = dayjs.utc('2024-12-01', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-12-01', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '1010', site_id: '1010',
xmltv_id: 'La1.es' xmltv_id: 'La1.es'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe(`https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/${date.format('YYYYMMDD')}_8h_1.json`) expect(url({ date })).toBe(
}) `https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/${date.format(
'YYYYMMDD'
it('can parse response', async () => { )}_8h_1.json`
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')).toString() )
let results = await parser({ content, channel, date }) })
results = results.map(p => {
p.start = p.start.toJSON() it('can parse response', async () => {
p.stop = p.stop.toJSON() const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')).toString()
return p let results = await parser({ content, channel, date })
}) results = results.map(p => {
p.start = p.start.toJSON()
expect(results.length).toBe(4) p.stop = p.stop.toJSON()
return p
var sampleResult = results[0]; })
expect(sampleResult).toMatchObject({ expect(results.length).toBe(4)
start: '2024-11-30T22:36:51.000Z',
stop: '2024-11-30T23:57:25.000Z', var sampleResult = results[0]
category: ['Cine', 'Romance', 'Comedia', 'Comedia Romántica'],
description: 'Charlie trabaja como director en una escuela de primaria y goza de una placentera existencia junto a sus amigos. A pesar de ello, no es feliz porque cada vez que se enamora pierde la cordura.', expect(sampleResult).toMatchObject({
title: 'Loco de amor' start: '2024-11-30T22:36:51.000Z',
}) stop: '2024-11-30T23:57:25.000Z',
}) category: ['Cine', 'Romance', 'Comedia', 'Comedia Romántica'],
description:
it('can handle empty guide', () => { 'Charlie trabaja como director en una escuela de primaria y goza de una placentera existencia junto a sus amigos. A pesar de ello, no es feliz porque cada vez que se enamora pierde la cordura.',
const result = parser({ title: 'Loco de amor'
date, })
channel, })
content: '{}'
}) it('can handle empty guide', () => {
expect(result).toMatchObject({}) const result = parser({
}) date,
channel,
content: '{}'
})
expect(result).toMatchObject({})
})

View file

@ -5,7 +5,12 @@ const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const packages = { 'OSNTV CONNECT': 3720, 'OSNTV PRIME': 3733, 'ALFA': 1281, 'OSN PINOY PLUS EXTRA': 3519 } const packages = {
'OSNTV CONNECT': 3720,
'OSNTV PRIME': 3733,
ALFA: 1281,
'OSN PINOY PLUS EXTRA': 3519
}
const country = 'AE' const country = 'AE'
const tz = 'Asia/Dubai' const tz = 'Asia/Dubai'
@ -13,11 +18,9 @@ module.exports = {
site: 'osn.com', site: 'osn.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${ return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${encodeURIComponent(
encodeURIComponent(date.format('MM/DD/YYYY')) date.format('MM/DD/YYYY')
}&co=${country}&ch=${ )}&co=${country}&ch=${channel.site_id}&mo=false&hr=0`
channel.site_id
}&mo=false&hr=0`
}, },
request: { request: {
headers({ channel }) { headers({ channel }) {
@ -46,7 +49,9 @@ module.exports = {
const axios = require('axios') const axios = require('axios')
for (const pkg of Object.values(packages)) { for (const pkg of Object.values(packages)) {
const channels = await axios const channels = await axios
.get(`https://www.osn.com/api/tvchannels.ashx?culture=en-US&packageId=${pkg}&country=${country}`) .get(
`https://www.osn.com/api/tvchannels.ashx?culture=en-US&packageId=${pkg}&country=${country}`
)
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)

View file

@ -28,32 +28,30 @@ it('can generate valid url', () => {
}) })
it('can parse response (ar)', () => { it('can parse response (ar)', () => {
const result = parser({ date, channel: channelAR, content }) const result = parser({ date, channel: channelAR, content }).map(a => {
.map(a => { a.start = a.start.toJSON()
a.start = a.start.toJSON() a.stop = a.stop.toJSON()
a.stop = a.stop.toJSON() return a
return a })
})
expect(result.length).toBe(29) expect(result.length).toBe(29)
expect(result[1]).toMatchObject({ expect(result[1]).toMatchObject({
start: '2024-11-26T20:50:00.000Z', start: '2024-11-26T20:50:00.000Z',
stop: '2024-11-26T21:45:00.000Z', stop: '2024-11-26T21:45:00.000Z',
title: 'بيت الحلويات: الحلقة 3', title: 'بيت الحلويات: الحلقة 3'
}) })
}) })
it('can parse response (en)', () => { it('can parse response (en)', () => {
const result = parser({ date, channel: channelEN, content }) const result = parser({ date, channel: channelEN, content }).map(a => {
.map(a => { a.start = a.start.toJSON()
a.start = a.start.toJSON() a.stop = a.stop.toJSON()
a.stop = a.stop.toJSON() return a
return a })
})
expect(result.length).toBe(29) expect(result.length).toBe(29)
expect(result[1]).toMatchObject({ expect(result[1]).toMatchObject({
start: '2024-11-26T20:50:00.000Z', start: '2024-11-26T20:50:00.000Z',
stop: '2024-11-26T21:45:00.000Z', stop: '2024-11-26T21:45:00.000Z',
title: 'House Of Desserts: Episode 3', title: 'House Of Desserts: Episode 3'
}) })
}) })

View file

@ -27,7 +27,7 @@ function parseItems(content, date) {
let data let data
try { try {
data = JSON.parse(json) data = JSON.parse(json)
} catch (error) { } catch {
return [] return []
} }

View file

@ -1,174 +1,172 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
let apiVersion let apiVersion
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'pickx.be', site: 'pickx.be',
days: 2, days: 2,
setApiVersion: function (version) { setApiVersion: function (version) {
apiVersion = version apiVersion = version
}, },
getApiVersion: function () { getApiVersion: function () {
return apiVersion return apiVersion
}, },
fetchApiVersion: fetchApiVersion, fetchApiVersion: fetchApiVersion,
url: async function ({ channel, date }) { url: async function ({ channel, date }) {
if (!apiVersion) { if (!apiVersion) {
await fetchApiVersion() await fetchApiVersion()
} }
return `https://px-epg.azureedge.net/airings/${apiVersion}/${date.format( return `https://px-epg.azureedge.net/airings/${apiVersion}/${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}/channel/${channel.site_id}?timezone=Europe%2FBrussels` )}/channel/${channel.site_id}?timezone=Europe%2FBrussels`
}, },
request: { request: {
headers: { headers: {
Origin: 'https://www.pickx.be', Origin: 'https://www.pickx.be',
Referer: 'https://www.pickx.be/' Referer: 'https://www.pickx.be/'
} }
}, },
parser({ channel, content }) { parser({ channel, content }) {
const programs = [] const programs = []
if (content) { if (content) {
const items = JSON.parse(content) const items = JSON.parse(content)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.program.title, title: item.program.title,
sub_title: item.program.episodeTitle, sub_title: item.program.episodeTitle,
description: item.program.description, description: item.program.description,
category: item.program.translatedCategory?.[channel.lang] category: item.program.translatedCategory?.[channel.lang]
? item.program.translatedCategory[channel.lang] ? item.program.translatedCategory[channel.lang]
: item.program.category.split('.')[1], : item.program.category.split('.')[1],
image: item.program.posterFileName image: item.program.posterFileName
? `https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/${item.program.posterFileName}` ? `https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/${item.program.posterFileName}`
: null, : null,
season: item.program.seasonNumber, season: item.program.seasonNumber,
episode: item.program.episodeNumber, episode: item.program.episodeNumber,
actors: item.program.actors, actors: item.program.actors,
director: item.program.director ? [item.program.director] : null, director: item.program.director ? [item.program.director] : null,
start: dayjs.utc(item.programScheduleStart), start: dayjs.utc(item.programScheduleStart),
stop: dayjs.utc(item.programScheduleEnd) stop: dayjs.utc(item.programScheduleEnd)
}) })
}) })
} }
return programs return programs
}, },
async channels({ lang = '' }) { async channels({ lang = '' }) {
const query = { const query = {
operationName: 'getChannels', operationName: 'getChannels',
variables: { variables: {
language: lang, language: lang,
queryParams: {}, queryParams: {},
id: '0', id: '0',
params: { params: {
shouldReadFromCache: true shouldReadFromCache: true
} }
}, },
query: `query getChannels($language: String!, $queryParams: ChannelQueryParams, $id: String, $params: ChannelParams) { query: `query getChannels($language: String!, $queryParams: ChannelQueryParams, $id: String, $params: ChannelParams) {
channels(language: $language, queryParams: $queryParams, id: $id, params: $params) { channels(language: $language, queryParams: $queryParams, id: $id, params: $params) {
id id
channelReferenceNumber channelReferenceNumber
name name
callLetter callLetter
number number
logo { logo {
key key
url url
__typename __typename
} }
language language
hd hd
radio radio
replayable replayable
ottReplayable ottReplayable
playable playable
ottPlayable ottPlayable
recordable recordable
subscribed subscribed
cloudRecordable cloudRecordable
catchUpWindowInHours catchUpWindowInHours
isOttNPVREnabled isOttNPVREnabled
ottNPVRStart ottNPVRStart
subscription { subscription {
channelRef channelRef
subscribed subscribed
upselling { upselling {
upsellable upsellable
packages packages
__typename __typename
} }
__typename __typename
} }
packages packages
__typename __typename
} }
}` }`
} }
const result = await axios const result = await axios
.post('https://api.proximusmwc.be/tiams/v3/graphql', query) .post('https://api.proximusmwc.be/tiams/v3/graphql', query)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return ( return (
result?.data?.channels result?.data?.channels
.filter( .filter(
channel => channel =>
!channel.radio && (!lang || channel.language === (lang === 'de' ? 'ger' : lang)) !channel.radio && (!lang || channel.language === (lang === 'de' ? 'ger' : lang))
) )
.map(channel => { .map(channel => {
return { return {
lang: channel.language === 'ger' ? 'de' : channel.language, lang: channel.language === 'ger' ? 'de' : channel.language,
site_id: channel.id, site_id: channel.id,
name: channel.name name: channel.name
} }
}) || [] }) || []
) )
} }
} }
function fetchApiVersion() { async function fetchApiVersion() {
return new Promise(async (resolve, reject) => { // you'll never find what happened here :)
try { // load the pickx page and get the hash from the MWC configuration.
// you'll never find what happened here :) // it's not the best way to get the version but it's the only way to get it.
// load the pickx page and get the hash from the MWC configuration. const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids'
// it's not the best way to get the version but it's the only way to get it. const hashData = await axios
.get(hashUrl)
const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids'; .then(r => {
const re = /"hashes":\["(.*)"\]/
const hashData = await axios.get(hashUrl) const match = r.data.match(re)
.then(r => { if (match && match[1]) {
const re = /"hashes":\["(.*)"\]/ return match[1]
const match = r.data.match(re) } else {
if (match && match[1]) { throw new Error('React app version hash not found')
return match[1] }
} else { })
throw new Error('React app version hash not found') .catch(console.error)
}
}) const versionUrl = `https://www.pickx.be/api/s-${hashData}`
.catch(console.error); const response = await axios.get(versionUrl, {
headers: {
const versionUrl = `https://www.pickx.be/api/s-${hashData}` Origin: 'https://www.pickx.be',
Referer: 'https://www.pickx.be/'
const response = await axios.get(versionUrl, { }
headers: { })
Origin: 'https://www.pickx.be',
Referer: 'https://www.pickx.be/' return new Promise((resolve, reject) => {
} try {
}) if (response.status === 200) {
apiVersion = response.data.version
if (response.status === 200) { resolve()
apiVersion = response.data.version } else {
resolve() console.error(`Failed to fetch API version. Status: ${response.status}`)
} else { reject(`Failed to fetch API version. Status: ${response.status}`)
console.error(`Failed to fetch API version. Status: ${response.status}`) }
reject(`Failed to fetch API version. Status: ${response.status}`) } catch (error) {
} console.error('Error during fetchApiVersion:', error)
} catch (error) { reject(error)
console.error('Error during fetchApiVersion:', error) }
reject(error) })
} }
})
}

View file

@ -1,76 +1,69 @@
jest.mock('./pickx.be.config.js', () => { jest.mock('./pickx.be.config.js', () => {
const originalModule = jest.requireActual('./pickx.be.config.js') const originalModule = jest.requireActual('./pickx.be.config.js')
return { return {
...originalModule, ...originalModule,
fetchApiVersion: jest.fn(() => Promise.resolve()) fetchApiVersion: jest.fn(() => Promise.resolve())
} }
}) })
const { const { parser, url, request, setApiVersion } = require('./pickx.be.config.js')
parser,
url, const fs = require('fs')
request, const path = require('path')
fetchApiVersion, const dayjs = require('dayjs')
setApiVersion, const utc = require('dayjs/plugin/utc')
getApiVersion
} = require('./pickx.be.config.js') dayjs.extend(utc)
const fs = require('fs') const date = dayjs.utc('2023-12-13').startOf('d')
const path = require('path') const channel = {
const dayjs = require('dayjs') lang: 'fr',
const utc = require('dayjs/plugin/utc') site_id: 'UID0118',
xmltv_id: 'Vedia.be'
dayjs.extend(utc) }
const date = dayjs.utc('2023-12-13').startOf('d') beforeEach(() => {
const channel = { setApiVersion('mockedApiVersion')
lang: 'fr', })
site_id: 'UID0118',
xmltv_id: 'Vedia.be' it('can generate valid url', async () => {
} const generatedUrl = await url({ channel, date })
expect(generatedUrl).toBe(
beforeEach(() => { 'https://px-epg.azureedge.net/airings/mockedApiVersion/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels'
setApiVersion('mockedApiVersion') )
}) })
it('can generate valid url', async () => { it('can generate valid request headers', () => {
const generatedUrl = await url({ channel, date }) expect(request.headers).toMatchObject({
expect(generatedUrl).toBe( Origin: 'https://www.pickx.be',
`https://px-epg.azureedge.net/airings/mockedApiVersion/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels` Referer: 'https://www.pickx.be/'
) })
}) })
it('can generate valid request headers', () => { it('can parse response', () => {
expect(request.headers).toMatchObject({ const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
Origin: 'https://www.pickx.be', const result = parser({ content, channel, date }).map(p => {
Referer: 'https://www.pickx.be/' p.start = p.start.toJSON()
}) p.stop = p.stop.toJSON()
}) return p
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) expect(result[0]).toMatchObject({
const result = parser({ content, channel, date }).map(p => { start: '2023-12-12T23:55:00.000Z',
p.start = p.start.toJSON() stop: '2023-12-13T00:15:00.000Z',
p.stop = p.stop.toJSON() title: 'Le 22h30',
return p description: 'Le journal de vivre ici.',
}) category: 'Info',
image:
expect(result[0]).toMatchObject({ 'https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/250_250_4B990CC58066A7B2A660AFA0BDDE5C41.jpg'
start: '2023-12-12T23:55:00.000Z', })
stop: '2023-12-13T00:15:00.000Z', })
title: 'Le 22h30',
description: 'Le journal de vivre ici.', it('can handle empty guide', () => {
category: 'Info', const result = parser({
image: date,
'https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/250_250_4B990CC58066A7B2A660AFA0BDDE5C41.jpg' channel,
}) content: ''
}) })
expect(result).toMatchObject([])
it('can handle empty guide', () => { })
const result = parser({
date,
channel,
content: ''
})
expect(result).toMatchObject([])
})

View file

@ -1,102 +1,104 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'player.ee.co.uk', site: 'player.ee.co.uk',
days: 2, days: 2,
url({ date, channel, hour = 0 }) { url({ date, channel, hour = 0 }) {
return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${ return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${encodeURIComponent(
encodeURIComponent(channel.site_id) channel.site_id
}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2,'0')}Z/PT12H` )}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2, '0')}Z/PT12H`
}, },
request: { request: {
headers: { headers: {
Referer: 'https://player.ee.co.uk/' Referer: 'https://player.ee.co.uk/'
} }
}, },
async parser({ content, channel, date }) { async parser({ content, channel, date }) {
const programs = [] const programs = []
if (content) { if (content) {
const schedule = JSON.parse(content) const schedule = JSON.parse(content)
// fetch next 12 hours schedule // fetch next 12 hours schedule
const { url, request } = module.exports const { url, request } = module.exports
const nextSchedule = await axios const nextSchedule = await axios
.get(url({ channel, date, hour: 12 }), { headers: request.headers }) .get(url({ channel, date, hour: 12 }), { headers: request.headers })
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
if (schedule?.items) { if (schedule?.items) {
// merge schedules // merge schedules
if (nextSchedule?.items) { if (nextSchedule?.items) {
schedule.items.push(...nextSchedule.items) schedule.items.push(...nextSchedule.items)
} }
schedule.items.forEach(item => { schedule.items.forEach(item => {
let season, episode let season, episode
const start = dayjs.utc(item.publishedStartTime) const start = dayjs.utc(item.publishedStartTime)
const stop = start.add(item.publishedDuration, 's') const stop = start.add(item.publishedDuration, 's')
const description = item.synopsis const description = item.synopsis
if (description) { if (description) {
const matches = description.trim().match(/\(?S(\d+)[\/\s]Ep(\d+)\)?/) const matches = description.trim().match(/\(?S(\d+)[/\s]Ep(\d+)\)?/)
if (matches) { if (matches) {
if (matches[1]) { if (matches[1]) {
season = parseInt(matches[1]) season = parseInt(matches[1])
} }
if (matches[2]) { if (matches[2]) {
episode = parseInt(matches[2]) episode = parseInt(matches[2])
} }
} }
} }
programs.push({ programs.push({
title: item.title, title: item.title,
description, description,
season, season,
episode, episode,
start, start,
stop stop
}) })
}) })
} }
} }
return programs return programs
}, },
async channels() { async channels() {
const token = const token =
'eyJkaXNjb3ZlcnlVc2VyR3JvdXBzIjpbIkFMTFVTRVJTIiwiYWxsIiwiaHR0cDovL3JlZmRhd' + 'eyJkaXNjb3ZlcnlVc2VyR3JvdXBzIjpbIkFMTFVTRVJTIiwiYWxsIiwiaHR0cDovL3JlZmRhd' +
'GEueW91dmlldy5jb20vbXBlZzdjcy9Zb3VWaWV3QXBwbGljYXRpb25QbGF5ZXJDUy8yMDIxLT' + 'GEueW91dmlldy5jb20vbXBlZzdjcy9Zb3VWaWV3QXBwbGljYXRpb25QbGF5ZXJDUy8yMDIxLT' +
'A5LTEwI2FuZHJvaWRfcnVudGltZS1wcm9maWxlMSIsInRhZzpidC5jb20sMjAxOC0wNy0xMTp' + 'A5LTEwI2FuZHJvaWRfcnVudGltZS1wcm9maWxlMSIsInRhZzpidC5jb20sMjAxOC0wNy0xMTp' +
'1c2VyZ3JvdXAjR0JSLWJ0X25vd1RWX211bHRpY2FzdCIsInRhZzpidC5jb20sMjAyMS0xMC0y' + '1c2VyZ3JvdXAjR0JSLWJ0X25vd1RWX211bHRpY2FzdCIsInRhZzpidC5jb20sMjAyMS0xMC0y' +
'NTp1c2VyZ3JvdXAjR0JSLWJ0X2V1cm9zcG9ydCJdLCJyZWdpb25zIjpbIkFMTFJFR0lPTlMiL' + 'NTp1c2VyZ3JvdXAjR0JSLWJ0X2V1cm9zcG9ydCJdLCJyZWdpb25zIjpbIkFMTFJFR0lPTlMiL' +
'CJHQlIiLCJHQlItRU5HIiwiR0JSLUVORy1sb25kb24iLCJhbGwiXSwic3Vic2V0IjoiMy41Lj' + 'CJHQlIiLCJHQlItRU5HIiwiR0JSLUVORy1sb25kb24iLCJhbGwiXSwic3Vic2V0IjoiMy41Lj' +
'EvYW5kcm9pZF9ydW50aW1lLXByb2ZpbGUxL0JST0FEQ0FTVF9JUC9HQlItYnRfYnJvYWRiYW5' + 'EvYW5kcm9pZF9ydW50aW1lLXByb2ZpbGUxL0JST0FEQ0FTVF9JUC9HQlItYnRfYnJvYWRiYW5' +
'kIiwic3Vic2V0cyI6WyIvLy8iLCIvL0JST0FEQ0FTVF9JUC8iLCIzLjUvLy8iXX0=' 'kIiwic3Vic2V0cyI6WyIvLy8iLCIvL0JST0FEQ0FTVF9JUC8iLCIzLjUvLy8iXX0='
const extensions = [ const extensions = [
'LinearCategoriesExtension', 'LinearCategoriesExtension',
'LogicalChannelNumberExtension', 'LogicalChannelNumberExtension',
'BTSubscriptionCodesExtension' 'BTSubscriptionCodesExtension'
] ]
const result = await axios const result = await axios
.get(`https://api.youview.tv/metadata/linear/v2/linear-services`, { .get('https://api.youview.tv/metadata/linear/v2/linear-services', {
params: { params: {
contentTargetingToken: token, contentTargetingToken: token,
extensions: extensions.join(',') extensions: extensions.join(',')
}, },
headers: module.exports.request.headers headers: module.exports.request.headers
}) })
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
return result?.items return (
.filter(channel => channel.contentTypes.indexOf('tv') >= 0) result?.items
.map(channel => { .filter(channel => channel.contentTypes.indexOf('tv') >= 0)
return { .map(channel => {
lang: 'en', return {
site_id: channel.serviceLocator, lang: 'en',
name: channel.fullName site_id: channel.serviceLocator,
} name: channel.fullName
}) || [] }
} }) || []
} )
}
}

View file

@ -1,72 +1,74 @@
const { parser, url } = require('./player.ee.co.uk.config.js') const { parser, url } = require('./player.ee.co.uk.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2023-12-13').startOf('d') const date = dayjs.utc('2023-12-13').startOf('d')
const channel = { const channel = {
site_id: 'dvb://233a..6d60', site_id: 'dvb://233a..6d60',
xmltv_id: 'HGTV.uk' xmltv_id: 'HGTV.uk'
} }
axios.get.mockImplementation((url, opts) => { axios.get.mockImplementation(url => {
if (url === 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T12Z/PT12H') { if (
return Promise.resolve({ url ===
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json'))) 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T12Z/PT12H'
}) ) {
} return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json')))
return Promise.resolve({ data: '' }) })
}) }
it('can generate valid url', () => { return Promise.resolve({ data: '' })
expect(url({ date, channel })).toBe( })
'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T00Z/PT12H'
) it('can generate valid url', () => {
}) expect(url({ date, channel })).toBe(
'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T00Z/PT12H'
it('can parse response', async () => { )
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) })
const result = (await parser({ content, channel, date }))
.map(p => { it('can parse response', async () => {
p.start = p.start.toJSON() const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
p.stop = p.stop.toJSON() const result = (await parser({ content, channel, date })).map(p => {
return p p.start = p.start.toJSON()
}) p.stop = p.stop.toJSON()
return p
expect(result).toMatchObject([ })
{
title: 'Bargain Mansions', expect(result).toMatchObject([
description: {
'Tamara and her dad help a recent widow who loves to cook for her family design her dream kitchen, perfect for entertaining and large gatherings. S4/Ep1', title: 'Bargain Mansions',
season: 4, description:
episode: 1, 'Tamara and her dad help a recent widow who loves to cook for her family design her dream kitchen, perfect for entertaining and large gatherings. S4/Ep1',
start: '2023-12-13T13:00:00.000Z', season: 4,
stop: '2023-12-13T14:00:00.000Z' episode: 1,
}, start: '2023-12-13T13:00:00.000Z',
{ stop: '2023-12-13T14:00:00.000Z'
title: 'Flip Or Flop', },
description: {
'Tarek and Christina are contacted by a cash strapped flipper who needs to unload a project house. S2/Ep2', title: 'Flip Or Flop',
season: 2, description:
episode: 2, 'Tarek and Christina are contacted by a cash strapped flipper who needs to unload a project house. S2/Ep2',
start: '2023-12-13T14:00:00.000Z', season: 2,
stop: '2023-12-13T14:30:00.000Z' episode: 2,
} start: '2023-12-13T14:00:00.000Z',
]) stop: '2023-12-13T14:30:00.000Z'
}) }
])
it('can handle empty guide', async () => { })
const result = await parser({
channel, it('can handle empty guide', async () => {
date, const result = await parser({
content: '' channel,
}) date,
expect(result).toMatchObject([]) content: ''
}) })
expect(result).toMatchObject([])
})

View file

@ -43,7 +43,7 @@ module.exports = {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.post( .post(
`https://playtv.unifi.com.my:7053/VSP/V3/QueryAllChannel`, 'https://playtv.unifi.com.my:7053/VSP/V3/QueryAllChannel',
{ isReturnAllMedia: '0' }, { isReturnAllMedia: '0' },
{ {
params: { params: {
@ -74,7 +74,7 @@ function parseItems(content, channel) {
const channelData = data.find(i => i.id == channel.site_id) const channelData = data.find(i => i.id == channel.site_id)
return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] return channelData.items && Array.isArray(channelData.items) ? channelData.items : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -1,50 +1,49 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const axios = require('axios')
dayjs.extend(utc)
dayjs.extend(utc) dayjs.extend(timezone)
dayjs.extend(timezone)
module.exports = {
module.exports = { site: 'pluto.tv',
site: 'pluto.tv', days: 3,
days: 3,
url: function ({ date, channel }) {
url: function ({ date, channel }) { const channelId = channel.site_id
const channelId = channel.site_id
const localTimezone = dayjs.tz.guess()
const localTimezone = dayjs.tz.guess()
const startTime = dayjs(date).tz(localTimezone).startOf('day').toISOString()
const startTime = dayjs(date).tz(localTimezone).startOf('day').toISOString() const endTime = dayjs(date).tz(localTimezone).add(this.days, 'day').endOf('day').toISOString()
const endTime = dayjs(date).tz(localTimezone).add(this.days, 'day').endOf('day').toISOString()
const generatedUrl = `https://api.pluto.tv/v2/channels/${channelId}?start=${startTime}&stop=${endTime}`
const generatedUrl = `https://api.pluto.tv/v2/channels/${channelId}?start=${startTime}&stop=${endTime}` return generatedUrl
return generatedUrl },
},
parser: function ({ content }) {
parser: function ({ content }) { const data = JSON.parse(content)
const data = JSON.parse(content) const programs = []
const programs = []
if (data.timelines) {
if (data.timelines) { data.timelines.forEach(item => {
data.timelines.forEach(item => { programs.push({
programs.push({ title: item.title,
title: item.title, subTitle: item.episode?.name || '',
subTitle: item.episode?.name || '', description: item.episode?.description || '',
description: item.episode?.description || '', episode: item.episode?.number || '',
episode: item.episode?.number || '', season: item.episode?.season || '',
season: item.episode?.season || '', actors: item.episode?.clip?.actors || [],
actors: item.episode?.clip?.actors || [], categories: [item.episode?.genre, item.episode?.subGenre].filter(Boolean),
categories: [item.episode?.genre, item.episode?.subGenre].filter(Boolean), rating: item.episode?.rating || '',
rating: item.episode?.rating || '', date: item.episode?.clip?.originalReleaseDate || '',
date: item.episode?.clip?.originalReleaseDate || '', icon: item.episode?.series?.tile?.path || '',
icon: item.episode?.series?.tile?.path || '', start: item.start,
start: item.start, stop: item.stop
stop: item.stop })
}) })
}) }
}
return programs
return programs }
} }
}

View file

@ -33,14 +33,17 @@ it('can parse response', () => {
start: '2024-12-28T00:21:00.000Z', start: '2024-12-28T00:21:00.000Z',
stop: '2024-12-28T00:48:00.000Z', stop: '2024-12-28T00:48:00.000Z',
title: 'Naruto: El Tercer Hokage, Eternamente', title: 'Naruto: El Tercer Hokage, Eternamente',
description: 'Gaara y Naruto continúan combatiendo con todas sus fuerzas. Decidido a proteger a Sakura, Naruto ataca a Gaara una y otra vez.', description:
'Gaara y Naruto continúan combatiendo con todas sus fuerzas. Decidido a proteger a Sakura, Naruto ataca a Gaara una y otra vez.',
subTitle: 'El Tercer Hokage, Eternamente', subTitle: 'El Tercer Hokage, Eternamente',
episode: 80, episode: 80,
season: 2, season: 2,
actors: ["Isabel Martion (Naruto Uzumaki)", actors: [
"Christine Byrd (Sakura Haruno)", 'Isabel Martion (Naruto Uzumaki)',
"Victor Ugarte (Sasuke Uchiha)", 'Christine Byrd (Sakura Haruno)',
"Alfonso Obreg (Kakashi Hatake)"], 'Victor Ugarte (Sasuke Uchiha)',
'Alfonso Obreg (Kakashi Hatake)'
],
categories: ['Anime', 'Anime Action & Adventure'], categories: ['Anime', 'Anime Action & Adventure'],
rating: 'TV-14', rating: 'TV-14',
date: '2004-04-21T00:00:00.000Z', date: '2004-04-21T00:00:00.000Z',

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels> <channels>
<channel site="pluto.tv" lang="en" xmltv_id="PlutoTVAdventCalendar.ca" site_id="6712256349c4060008e9e0f0">Pluto TV Advent Calendar</channel> <channel site="pluto.tv" lang="en" xmltv_id="PlutoTVAdventCalendar.ca" site_id="6712256349c4060008e9e0f0">Pluto TV Advent Calendar</channel>

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels> <channels>
<channel site="pluto.tv" lang="en" xmltv_id="Diane,femmeflic.uk" site_id="6671b26836a2f90008d9333c">Diane, femme flic</channel> <channel site="pluto.tv" lang="en" xmltv_id="Diane,femmeflic.uk" site_id="6671b26836a2f90008d9333c">Diane, femme flic</channel>

View file

@ -46,10 +46,10 @@ module.exports = {
return programs return programs
}, },
async channels({ country, lang }) { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://www.programetv.ro/api/station/index/`) .get('https://www.programetv.ro/api/station/index/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -62,7 +62,7 @@ module.exports = {
$('.channelList-listItemsLink').each((i, el) => { $('.channelList-listItemsLink').each((i, el) => {
const name = $(el).attr('title') const name = $(el).attr('title')
const url = $(el).attr('href') const url = $(el).attr('href')
const [, site_id] = url.match(/\/programme\-(.*)\.html$/i) const [, site_id] = url.match(/\/programme-(.*)\.html$/i)
channels.push({ channels.push({
lang: 'fr', lang: 'fr',

Some files were not shown because too many files have changed in this diff Show more