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 = {
tabWidth: 2,
useTabs: false,
endOfLine: 'lf',
endOfLine: 'crlf',
semi: false,
singleQuote: true,
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
<!-- prettier-ignore -->
#include "./.sites/_table.md"
# Sites
<!-- prettier-ignore -->
#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.
- `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).
- `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.

427
SITES.md
View file

@ -1,212 +1,215 @@
# Sites
<!-- prettier-ignore -->
<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/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/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>https://github.com/iptv-org/epg/issues/2556</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>https://github.com/iptv-org/epg/issues/2557</td></tr>
<tr><td><a href="sites/ontvtonight.com">ontvtonight.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/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></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/tivie.id">tivie.id</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>https://github.com/iptv-org/epg/issues/2555</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>
# Sites
<!-- prettier-ignore -->
<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></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/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/cyta.com.cy">cyta.com.cy</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></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>https://github.com/iptv-org/epg/issues/2556</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>https://github.com/iptv-org/epg/issues/2557</td></tr>
<tr><td><a href="sites/ontvtonight.com">ontvtonight.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/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/pluto.tv">pluto.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></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/stod2.is">stod2.is</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/tivie.id">tivie.id</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>https://github.com/iptv-org/epg/issues/2570</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>

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",
"dependencies": {
"@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@freearhey/core": "^0.3.1",
"@freearhey/search-js": "^0.1.1",
"@ntlab/sfetch": "^1.0.0",
"@octokit/plugin-paginate-rest": "^11.3.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/fs-extra": "^11.0.2",
"@types/inquirer": "^9.0.3",
@ -39,9 +43,12 @@
"form-data": "^4.0.0",
"fs-extra": "^10.0.1",
"glob": "^7.2.0",
"globals": "^15.14.0",
"husky": "^9.1.7",
"iconv-lite": "^0.4.24",
"inquirer": "^8.2.6",
"jest": "^29.7.0",
"jest-offline": "^1.0.1",
"langs": "^2.0.0",
"libxmljs2": "^0.35.0",
"lodash": "^4.17.21",
@ -58,12 +65,12 @@
"run-script-os": "^1.1.6",
"serve": "^14.2.4",
"signale": "^1.4.0",
"skip-postinstall": "^1.0.0",
"srcset": "^4.0.0",
"table2array": "^0.0.2",
"tabletojson": "^2.0.7",
"tough-cookie": "^5.0.0",
"transliteration": "^2.2.0",
"ts-jest": "^29.1.1",
"tsx": "^4.19.2",
"unzipit": "^1.4.0",
"wildcard-match": "^5.1.2"
@ -639,6 +646,14 @@
"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": {
"version": "7.23.3",
"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": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
@ -2246,6 +2272,222 @@
"@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": {
"version": "4.0.6",
"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_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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -5262,11 +5493,14 @@
}
},
"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==",
"version": "15.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
"engines": {
"node": ">=4"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/got": {
@ -5417,6 +5651,20 @@
"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": {
"version": "0.4.24",
"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_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": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
@ -6562,6 +6818,11 @@
"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": {
"version": "6.1.0",
"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",
"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": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -6796,7 +7052,9 @@
"node_modules/make-error": {
"version": "1.3.6",
"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": {
"version": "13.0.1",
@ -7083,6 +7341,25 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"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": {
"version": "1.0.4",
"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",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -9406,86 +9692,6 @@
"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": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@ -10484,6 +10690,13 @@
"@babel/types": "^7.23.3",
"debug": "^4.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": {
@ -10994,6 +11207,14 @@
"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": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
@ -11591,6 +11812,108 @@
"@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": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -12372,14 +12695,6 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -13782,9 +14097,9 @@
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
"version": "15.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="
},
"got": {
"version": "11.8.5",
@ -13888,6 +14203,11 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"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": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -14438,6 +14758,14 @@
"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": {
"version": "1.2.3",
"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",
"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": {
"version": "6.1.0",
"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",
"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": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -14908,7 +15236,9 @@
"make-error": {
"version": "1.3.6",
"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": {
"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": {
"version": "1.0.4",
"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",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -16818,49 +17168,6 @@
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
"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": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import chalk from 'chalk'
import libxml, { ValidationError } from 'libxmljs2'
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"?>
<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:schema>`
program
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
const options = program.opts()
program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv)
async function main() {
const logger = new Logger()
const storage = new Storage()
logger.info('options:')
logger.tree(options)
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) {
const file = new File(filepath)
if (file.extension() !== 'xml') continue
@ -51,11 +39,15 @@ async function main() {
let localErrors: ValidationError[] = []
const xsdDoc = libxml.parseXml(xsd)
const doc = libxml.parseXml(xml)
try {
const xsdDoc = libxml.parseXml(xsd)
const doc = libxml.parseXml(xml)
if (!doc.validate(xsdDoc)) {
localErrors = doc.validationErrors
if (!doc.validate(xsdDoc)) {
localErrors = doc.validationErrors
}
} catch (error) {
localErrors.push(error)
}
if (localErrors.length) {

View file

@ -2,7 +2,7 @@ import { Logger, File, Collection, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core'
import { Channel } from 'epg-grabber'
import { Command } from 'commander'
import path from 'path'
import { pathToFileURL } from 'node:url'
const program = new Command()
program
@ -26,7 +26,7 @@ async function main() {
const logger = new Logger()
const file = new File(options.config)
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`
let channels = new Collection()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,57 +1,57 @@
import { Collection } from '@freearhey/core'
import { Issue } from './'
enum StatusCode {
DOWN = 'down',
WARNING = 'warning',
OK = 'ok'
}
type Status = {
code: StatusCode
emoji: string
}
type SiteProps = {
domain: string
issues: Collection
}
export class Site {
domain: string
issues: Collection
constructor({ domain, issues }: SiteProps) {
this.domain = domain
this.issues = issues
}
getStatus(): Status {
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
issue.labels.find(label => label === 'status:down')
)
if (issuesWithStatusDown.notEmpty())
return {
code: StatusCode.DOWN,
emoji: '🔴'
}
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
issue.labels.find(label => label === 'status:warning')
)
if (issuesWithStatusWarning.notEmpty())
return {
code: StatusCode.WARNING,
emoji: '🟡'
}
return {
code: StatusCode.OK,
emoji: '🟢'
}
}
getIssues(): Collection {
return this.issues.map((issue: Issue) => issue.getURL())
}
}
import { Collection } from '@freearhey/core'
import { Issue } from './'
enum StatusCode {
DOWN = 'down',
WARNING = 'warning',
OK = 'ok'
}
type Status = {
code: StatusCode
emoji: string
}
type SiteProps = {
domain: string
issues: Collection
}
export class Site {
domain: string
issues: Collection
constructor({ domain, issues }: SiteProps) {
this.domain = domain
this.issues = issues
}
getStatus(): Status {
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
issue.labels.find(label => label === 'status:down')
)
if (issuesWithStatusDown.notEmpty())
return {
code: StatusCode.DOWN,
emoji: '🔴'
}
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
issue.labels.find(label => label === 'status:warning')
)
if (issuesWithStatusWarning.notEmpty())
return {
code: StatusCode.WARNING,
emoji: '🟡'
}
return {
code: StatusCode.OK,
emoji: '🟢'
}
}
getIssues(): Collection {
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 channelData = data.schedule.find(i => i.channel == channelId)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch (err) {
} catch {
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 path = require('path')
const dayjs = require('dayjs')

View file

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

View file

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

View file

@ -1,54 +1,55 @@
const axios = require('axios');
const cheerio = require('cheerio');
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
const customParseFormat = require('dayjs/plugin/customParseFormat');
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);
module.exports = {
site: 'chada.ma',
channels: 'chada.ma.channels.xml',
days: 1,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url() {
return 'https://chada.ma/fr/chada-tv/grille-tv/';
},
parser: function ({ content }) {
const $ = cheerio.load(content);
const programs = [];
$('#stopfix .posts-area h2').each((i, element) => {
const timeRange = $(element).text().trim();
const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim()));
const titleElement = $(element).next('div').next('h3');
const title = titleElement.text().trim();
const description = titleElement.next('div').text().trim() || 'No description available';
programs.push({
title,
description,
start,
stop
});
});
return programs;
}
};
function parseProgramTime(timeStr) {
const timeZone = 'Africa/Casablanca';
const currentDate = dayjs().format('YYYY-MM-DD');
return dayjs.tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone).format('YYYY-MM-DDTHH:mm:ssZ');
}
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'chada.ma',
channels: 'chada.ma.channels.xml',
days: 1,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url() {
return 'https://chada.ma/fr/chada-tv/grille-tv/'
},
parser: function ({ content }) {
const $ = cheerio.load(content)
const programs = []
$('#stopfix .posts-area h2').each((i, element) => {
const timeRange = $(element).text().trim()
const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim()))
const titleElement = $(element).next('div').next('h3')
const title = titleElement.text().trim()
const description = titleElement.next('div').text().trim() || 'No description available'
programs.push({
title,
description,
start,
stop
})
})
return programs
}
}
function parseProgramTime(timeStr) {
const timeZone = 'Africa/Casablanca'
const currentDate = dayjs().format('YYYY-MM-DD')
return dayjs
.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 axios = require('axios')
const dayjs = require('dayjs')
const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
jest.mock('axios')
const mockHtmlContent = `
<div class="pm0 col-md-8" id="stopfix">
<h2 class="posts-date">Programmes d'Aujourd'hui</h2>
<div class="posts-area">
<h2> <i class="fas fa-circle"></i>00:00 - 09:00</h2>
<div class="relativeme">
<a href="https://chada.ma/fr/emissions/bloc-prime-clips/">
<img class="programthumb" src="https://chada.ma/wp-content/uploads/2023/11/Autres-slides-clips-la-couverture.jpg">
</a>
</div>
<h3>Bloc Prime + Clips</h3>
<div class="authorbox"></div>
<div class="ssprogramme row"></div>
</div>
</div>
`;
it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
});
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')
p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
return p
})
expect(result).toMatchObject([
{
title: "Bloc Prime + Clips",
description: "No description available",
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({
content: '<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./chada.ma.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
jest.mock('axios')
const mockHtmlContent = `
<div class="pm0 col-md-8" id="stopfix">
<h2 class="posts-date">Programmes d'Aujourd'hui</h2>
<div class="posts-area">
<h2> <i class="fas fa-circle"></i>00:00 - 09:00</h2>
<div class="relativeme">
<a href="https://chada.ma/fr/emissions/bloc-prime-clips/">
<img class="programthumb" src="https://chada.ma/wp-content/uploads/2023/11/Autres-slides-clips-la-couverture.jpg">
</a>
</div>
<h3>Bloc Prime + Clips</h3>
<div class="authorbox"></div>
<div class="ssprogramme row"></div>
</div>
</div>
`
it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
})
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')
p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
return p
})
expect(result).toMatchObject([
{
title: 'Bloc Prime + Clips',
description: 'No description available',
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({
content: '<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>'
})
expect(result).toMatchObject([])
})

View file

@ -16,9 +16,12 @@ module.exports = {
const start = parseStart(item)
const stop = parseStop(item, start)
programs.push({
title: item.season?.serie?.title ? item.season.serie.title : item.title,
title: item.title,
subTitle: item.season?.serie?.title,
category: item.genreDetailed,
description: item.synopsis,
season: parseSeason(item),
episode: parseEpisode(item),
image: parseImage(item),
start: start.toJSON(),
stop: stop.toJSON()
@ -29,7 +32,7 @@ module.exports = {
},
async channels() {
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)
.catch(console.log)
@ -61,6 +64,14 @@ function parseStop(item, start) {
return start.add(item.duration, 's')
}
function parseSeason(item) {
return item.season?.number
}
function parseEpisode(item) {
return item.episodeNumber
}
function parseItems(content, channel) {
const data = JSON.parse(content)

View file

@ -27,6 +27,9 @@ it('can parse response', () => {
start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste',
subTitle: 'Esprits criminels',
season: 10,
episode: 12,
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.",
category: 'Série Suspense',

View file

@ -40,7 +40,7 @@ module.exports = {
},
async channels() {
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)
.catch(console.log)
@ -85,7 +85,7 @@ function parseItems(content, date) {
const schedules = data.response.schedule
return schedules[date.format('YYYY-MM-DD')] || []
} catch (e) {
} catch {
return []
}
}

View file

@ -1,81 +1,85 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
module.exports = {
site: 'cosmotetv.gr',
days: 5,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
},
method: 'GET',
headers: {
'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',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Origin': 'https://www.cosmotetv.gr',
'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site'
}
},
url: function ({date, channel}) {
const startOfDay = dayjs(date).startOf('day').utc().unix()
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 = []
const data = JSON.parse(content)
data.channels.forEach(channel => {
channel.items.forEach(item => {
const start = dayjs(item.startTime).utc().toISOString()
const stop = dayjs(item.endTime).utc().toISOString()
programs.push({
title: item.title,
description: item.description || 'No description available',
category: item.qoe.genre,
image: item.thumbnails.standard,
start,
stop
})
})
})
return programs
},
async channels() {
const axios = require('axios')
try {
const response = await axios.get('https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el', {
headers: this.request.headers
})
const data = response.data
if (data && data.channels) {
return data.channels.map(item => ({
lang: 'el',
site_id: item.callSign,
name: item.title,
//logo: item.logos.square
}))
} else {
console.error('Unexpected response structure:', data)
return []
}
} catch (error) {
console.error('Error fetching channel data:', error)
return []
}
}
}
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
module.exports = {
site: 'cosmotetv.gr',
days: 5,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
},
method: 'GET',
headers: {
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',
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br, zstd',
Origin: 'https://www.cosmotetv.gr',
'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site'
}
},
url: function ({ date, channel }) {
const startOfDay = dayjs(date).startOf('day').utc().unix()
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 ({ content }) {
let programs = []
const data = JSON.parse(content)
data.channels.forEach(channel => {
channel.items.forEach(item => {
const start = dayjs(item.startTime).utc().toISOString()
const stop = dayjs(item.endTime).utc().toISOString()
programs.push({
title: item.title,
description: item.description || 'No description available',
category: item.qoe.genre,
image: item.thumbnails.standard,
start,
stop
})
})
})
return programs
},
async channels() {
const axios = require('axios')
try {
const response = await axios.get(
'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el',
{
headers: this.request.headers
}
)
const data = response.data
if (data && data.channels) {
return data.channels.map(item => ({
lang: 'el',
site_id: item.callSign,
name: item.title
//logo: item.logos.square
}))
} else {
console.error('Unexpected response structure:', data)
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
const axios = require('axios')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
const mockChannelData = {
"channels": [
{
"guid": "XTV100000954",
"title": "ΒΟΥΛΗ HD",
"callSign": "vouli",
"logos": {
"square": "https://tr.static.cdn.cosmotetvott.gr/ote-prod/channel_logos/vouli1-normal.png",
"wide": "https://tr.static.cdn.cosmotetvott.gr/ote-prod/channel_logos/vouli1-wide.png"
}
}
]
}
const mockEpgData = {
"channels": [
{
"items": [
{
"startTime": "2024-12-26T23:00:00+00:00",
"endTime": "2024-12-27T00:00:00+00:00",
"title": "Τι Λέει ο Νόμος",
"description": "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.",
"qoe": {
"genre": "Special"
},
"thumbnails": {
"standard": "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg"
}
}
]
}
]
}
it('can generate valid url', () => {
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`)
})
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()
return p
})
expect(result).toMatchObject([
{
title: "Τι Λέει ο Νόμος",
description: "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.",
category: "Special",
image: "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg",
start: "2024-12-26T23:00:00.000Z",
stop: "2024-12-27T00:00:00.000Z"
}
])
})
it('can handle empty guide', () => {
const result = parser({ date, channel, content: '{"date":"2024-12-26","categories":[],"channels":[]}' });
expect(result).toMatchObject([])
})
const { parser, url } = require('./cosmotetv.gr.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
const mockEpgData = {
channels: [
{
items: [
{
startTime: '2024-12-26T23:00:00+00:00',
endTime: '2024-12-27T00:00:00+00:00',
title: 'Τι Λέει ο Νόμος',
description:
'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
qoe: {
genre: 'Special'
},
thumbnails: {
standard:
'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg'
}
}
]
}
]
}
it('can generate valid url', () => {
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`
)
})
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()
return p
})
expect(result).toMatchObject([
{
title: 'Τι Λέει ο Νόμος',
description:
'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
category: 'Special',
image:
'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg',
start: '2024-12-26T23:00:00.000Z',
stop: '2024-12-27T00:00:00.000Z'
}
])
})
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 timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone)
dayjs.extend(utc)
module.exports = {
site: 'cubmu.com',
days: 2,
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}`
},
parser({ content, channel }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: parseTitle(item),
description: parseDescription(item, channel.lang),
episode: parseEpisode(item),
start: parseStart(item).toISOString(),
stop: parseStop(item).toISOString()
})
})
return programs
},
async channels({ lang = 'id' }) {
const axios = require('axios')
const cheerio = require('cheerio')
const result = await axios
.get('https://cubmu.com/live-tv')
.then(response => response.data)
.catch(console.error)
const $ = cheerio.load(result)
// retrieve service api data
const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {}
const options = {
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)
.then(response => response.data)
.catch(console.error)
// list channels
const subscribedChannels = await axios
.post(`https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`, options)
.then(response => response.data)
.catch(console.error)
const channels = []
const included = []
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,
site_id: channel.id,
name: channel.name
})
}
})
})
}
return channels
}
}
function parseItems(content) {
return content ? JSON.parse(content.trim()).result || [] : []
}
function parseTitle(item) {
return item.scehedule_title
}
function parseDescription(item, lang = 'id') {
return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis
}
function parseEpisode(item) {
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')
}
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone)
dayjs.extend(utc)
module.exports = {
site: 'cubmu.com',
days: 2,
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}`
},
parser({ content, channel }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: parseTitle(item),
description: parseDescription(item, channel.lang),
episode: parseEpisode(item),
start: parseStart(item).toISOString(),
stop: parseStop(item).toISOString()
})
})
return programs
},
async channels({ lang = 'id' }) {
const axios = require('axios')
const cheerio = require('cheerio')
const result = await axios
.get('https://cubmu.com/live-tv')
.then(response => response.data)
.catch(console.error)
const $ = cheerio.load(result)
// retrieve service api data
const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {}
const options = {
headers: {
Origin: 'https://cubmu.com',
Referer: 'https://cubmu.com/live-tv'
}
}
// login to service bus
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
)
.then(response => response.data)
.catch(console.error)
// list channels
const subscribedChannels = await axios
.post(
`https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`,
options
)
.then(response => response.data)
.catch(console.error)
const channels = []
const included = []
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,
site_id: channel.id,
name: channel.name
})
}
})
})
}
return channels
}
}
function parseItems(content) {
return content ? JSON.parse(content.trim()).result || [] : []
}
function parseTitle(item) {
return item.scehedule_title
}
function parseDescription(item, lang = 'id') {
return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis
}
function parseEpisode(item) {
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' }
const channelEn = Object.assign({}, channel, { lang: 'en' })
it('can generate valid url', () => {
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'
)
})
it('can parse response', () => {
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"}]}'
const idResults = parser({ content, channel })
expect(idResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
description:
'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 })
expect(enResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
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.'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { url, parser } = require('./cubmu.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' }
const channelEn = Object.assign({}, channel, { lang: 'en' })
it('can generate valid url', () => {
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'
)
})
it('can parse response', () => {
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"}]}'
const idResults = parser({ content, channel })
expect(idResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
description:
'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 })
expect(enResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
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.'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View file

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

View file

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

View file

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

View file

@ -65,7 +65,7 @@ module.exports = {
const cheerio = require('cheerio')
const data = await axios
.get(`https://www.digiturk.com.tr/`, {
.get('https://www.digiturk.com.tr/', {
headers: {
'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'

View file

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

View file

@ -49,7 +49,7 @@ module.exports = {
const channels = []
for (let provider of providers) {
const data = await axios
.post(`https://www.guida.tv/guide/schedule`, null, {
.post('https://www.guida.tv/guide/schedule', null, {
params: {
provider,
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 dateString = `${date.format('YYYY-MM-DD')} ${timeString}`

View file

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

View file

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

View file

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

View file

@ -1,73 +1,80 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'ipko.tv',
timezone: 'Europe/Belgrade',
days: 5,
url({ date, channel }) { return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData' },
request: {
method: 'POST',
headers: {
'Host': 'stargate.ipko.tv',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/json',
'X-AppLayout': '1',
'x-language': 'sq',
'Origin': 'https://ipko.tv',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1',
'Connection': 'keep-alive'
},
data({ channel, date }) {
const todayEpoch = date.startOf('day').unix();
const nextDayEpoch = date.add(1, 'day').startOf('day').unix();
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 => {
const start = dayjs.unix(show.show_start).utc();
const stop = dayjs.unix(show.show_end).utc();
const programData = {
title: show.title,
description: show.summary || 'No description available',
start: start.toISOString(),
stop: stop.toISOString(),
thumbnail: show.thumbnail
}
programs.push(programData)
})
return programs
},
async channels() {
const response = await axios.post('https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList', JSON.stringify({ includeRadioStations: true }), {
headers: this.request.headers
});
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)
}))
}
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'ipko.tv',
timezone: 'Europe/Belgrade',
days: 5,
url() {
return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData'
},
request: {
method: 'POST',
headers: {
Host: 'stargate.ipko.tv',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/json',
'X-AppLayout': '1',
'x-language': 'sq',
Origin: 'https://ipko.tv',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1',
Connection: 'keep-alive'
},
data({ channel, date }) {
const todayEpoch = date.startOf('day').unix()
const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
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 => {
const start = dayjs.unix(show.show_start).utc()
const stop = dayjs.unix(show.show_end).utc()
const programData = {
title: show.title,
description: show.summary || 'No description available',
start: start.toISOString(),
stop: stop.toISOString(),
thumbnail: show.thumbnail
}
programs.push(programData)
})
return programs
},
async channels() {
const response = await axios.post(
'https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList',
JSON.stringify({ includeRadioStations: true }),
{
headers: this.request.headers
}
)
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2024-12-24', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'ipko-promo',
xmltv_id: 'IPKOPROMO'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData')
})
it('can parse response', () => {
const content = `
{
"shows": [
{
"title": "IPKO Promo",
"show_start": 1735012800,
"show_end": 1735020000,
"timestamp": "5:00 - 7:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105567",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false,
"friendly_id": "ipko_promo_4cf3",
"pg": "",
"genres": [],
"year": 0,
"summary": "",
"categories": "Other",
"stb_only": false,
"is_live": false,
"original_title": "IPKO Promo"
},
{
"title": "IPKO Promo",
"show_start": 1735020000,
"show_end": 1735027200,
"timestamp": "7:00 - 9:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105568",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false,
"friendly_id": "ipko_promo_416b",
"pg": "",
"genres": [],
"year": 0,
"summary": "",
"categories": "Other",
"stb_only": false,
"is_live": false,
"original_title": "IPKO Promo"
},
{
"title": "IPKO Promo",
"show_start": 1735027200,
"show_end": 1735034400,
"timestamp": "9:00 - 11:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105569",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false,
"friendly_id": "ipko_promo_2e23",
"pg": "",
"genres": [],
"year": 0,
"summary": "",
"categories": "Other",
"stb_only": false,
"is_live": false,
"original_title": "IPKO Promo"
}
]
}`
const result = parser({ content, channel }).map(p => {
p.start = p.start
p.stop = p.stop
return p
})
expect(result).toMatchObject([
{
title: "IPKO Promo",
description: "No description available",
start: "2024-12-24T04:00:00.000Z",
stop: "2024-12-24T06:00:00.000Z",
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg"
},
{
title: "IPKO Promo",
description: "No description available",
start: "2024-12-24T06:00:00.000Z",
stop: "2024-12-24T08:00:00.000Z",
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg"
},
{
title: "IPKO Promo",
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":[]}'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./ipko.tv.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2024-12-24', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'ipko-promo',
xmltv_id: 'IPKOPROMO'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData')
})
it('can parse response', () => {
const content = `
{
"shows": [
{
"title": "IPKO Promo",
"show_start": 1735012800,
"show_end": 1735020000,
"timestamp": "5:00 - 7:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105567",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false,
"friendly_id": "ipko_promo_4cf3",
"pg": "",
"genres": [],
"year": 0,
"summary": "",
"categories": "Other",
"stb_only": false,
"is_live": false,
"original_title": "IPKO Promo"
},
{
"title": "IPKO Promo",
"show_start": 1735020000,
"show_end": 1735027200,
"timestamp": "7:00 - 9:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105568",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false,
"friendly_id": "ipko_promo_416b",
"pg": "",
"genres": [],
"year": 0,
"summary": "",
"categories": "Other",
"stb_only": false,
"is_live": false,
"original_title": "IPKO Promo"
},
{
"title": "IPKO Promo",
"show_start": 1735027200,
"show_end": 1735034400,
"timestamp": "9:00 - 11:00",
"show_id": "EPG_TvProfil_IPKOPROMO_296105569",
"thumbnail": "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg",
"is_adult": false,
"friendly_id": "ipko_promo_2e23",
"pg": "",
"genres": [],
"year": 0,
"summary": "",
"categories": "Other",
"stb_only": false,
"is_live": false,
"original_title": "IPKO Promo"
}
]
}`
const result = parser({ content, channel })
expect(result).toMatchObject([
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T04:00:00.000Z',
stop: '2024-12-24T06:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
},
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T06:00:00.000Z',
stop: '2024-12-24T08:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
},
{
title: 'IPKO Promo',
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":[]}'
})
expect(result).toMatchObject([])
})

View file

@ -38,7 +38,7 @@ module.exports = {
async channels() {
const axios = require('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)
.catch(console.log)

View file

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

View file

@ -32,7 +32,7 @@ module.exports = {
async channels() {
const axios = require('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)
.catch(console.log)

View file

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

View file

@ -1,93 +1,97 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
module.exports = {
site: 'mediasetinfinity.mediaset.it',
days: 2,
url: function ({date, channel}) {
// Get the epoch timestamp
const todayEpoch = date.startOf('day').utc().valueOf()
// Get the epoch timestamp for the next day
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}`
},
parser: function ({content}) {
const programs = []
const data = JSON.parse(content)
if (!data.response || !data.response.entries || !data.response.entries[0] || !data.response.entries[0].listings) {
// If the structure is not as expected, return an empty array
return programs
}
const listings = data.response.entries[0].listings
listings.forEach((listing) => {
const title = listing.mediasetlisting$epgTitle
const subTitle = listing.program.title
const season = parseSeason(listing)
const episode = parseEpisode(listing)
if (listing.program.title && listing.startTime && listing.endTime) {
programs.push({
title: title || subTitle,
sub_title: title && title != subTitle ? subTitle : null,
description: listing.program.description || null,
category: listing.program.mediasetprogram$skyGenre || null,
season: episode && !season ? '0' : season,
episode: episode,
start: parseTime(listing.startTime),
stop: parseTime(listing.endTime),
image: getMaxResolutionThumbnails(listing)
})
}
})
return programs
}
}
function parseTime(timestamp) {
return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm')
}
function parseSeason(item) {
if (!item.mediasetlisting$shortDescription) return null
const season = item.mediasetlisting$shortDescription.match(/S(\d+)\s/)
return season ? season[1] : null
}
function parseEpisode(item) {
if (!item.mediasetlisting$shortDescription) return null
const episode = item.mediasetlisting$shortDescription.match(/Ep(\d+)\s/)
return episode ? episode[1] : null
}
function getMaxResolutionThumbnails(item) {
const thumbnails = item.program.thumbnails || null
const maxResolutionThumbnails = {}
for (const key in thumbnails) {
const type = key.split('-')[0] // Estrarre il tipo di thumbnail
const {width, height, url, title} = thumbnails[key]
if (!maxResolutionThumbnails[type] ||
(width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height)) {
maxResolutionThumbnails[type] = {width, height, url, title}
}
}
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
}
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
module.exports = {
site: 'mediasetinfinity.mediaset.it',
days: 2,
url: function ({ date, channel }) {
// Get the epoch timestamp
const todayEpoch = date.startOf('day').utc().valueOf()
// Get the epoch timestamp for the next day
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}`
},
parser: function ({ content }) {
const programs = []
const data = JSON.parse(content)
if (
!data.response ||
!data.response.entries ||
!data.response.entries[0] ||
!data.response.entries[0].listings
) {
// If the structure is not as expected, return an empty array
return programs
}
const listings = data.response.entries[0].listings
listings.forEach(listing => {
const title = listing.mediasetlisting$epgTitle
const subTitle = listing.program.title
const season = parseSeason(listing)
const episode = parseEpisode(listing)
if (listing.program.title && listing.startTime && listing.endTime) {
programs.push({
title: title || subTitle,
sub_title: title && title != subTitle ? subTitle : null,
description: listing.program.description || null,
category: listing.program.mediasetprogram$skyGenre || null,
season: episode && !season ? '0' : season,
episode: episode,
start: parseTime(listing.startTime),
stop: parseTime(listing.endTime),
image: getMaxResolutionThumbnails(listing)
})
}
})
return programs
}
}
function parseTime(timestamp) {
return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm')
}
function parseSeason(item) {
if (!item.mediasetlisting$shortDescription) return null
const season = item.mediasetlisting$shortDescription.match(/S(\d+)\s/)
return season ? season[1] : null
}
function parseEpisode(item) {
if (!item.mediasetlisting$shortDescription) return null
const episode = item.mediasetlisting$shortDescription.match(/Ep(\d+)\s/)
return episode ? episode[1] : null
}
function getMaxResolutionThumbnails(item) {
const thumbnails = item.program.thumbnails || null
const maxResolutionThumbnails = {}
for (const key in thumbnails) {
const type = key.split('-')[0] // Estrarre il tipo di thumbnail
const { width, height, url, title } = thumbnails[key]
if (
!maxResolutionThumbnails[type] ||
width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height
) {
maxResolutionThumbnails[type] = { width, height, url, title }
}
}
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 fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2024-01-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'LB', xmltv_id: '20.it'
}
it('can generate valid url', () => {
expect(url({
channel,
date
})).toBe('https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const results = parser({content, date}).map(p => {
return p
})
expect(results[3]).toMatchObject({
start: '2024-01-20 02:14',
stop: '2024-01-20 02:54',
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.',
category: 'Intrattenimento',
season: '7',
episode: '22',
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'
})
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./mediasetinfinity.mediaset.it.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2024-01-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'LB',
xmltv_id: '20.it'
}
it('can generate valid url', () => {
expect(
url({
channel,
date
})
).toBe(
'https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const results = parser({ content, date }).map(p => {
return p
})
expect(results[3]).toMatchObject({
start: '2024-01-20 02:14',
stop: '2024-01-20 02:54',
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.',
category: 'Intrattenimento',
season: '7',
episode: '22',
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'
})
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})

View file

@ -40,7 +40,7 @@ module.exports = {
async channels() {
const axios = require('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: {
Origin: 'https://www.meo.pt'
}

View file

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

View file

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

View file

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

View file

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

View file

@ -48,8 +48,10 @@ function parseItems(context) {
schDayPrograms.forEach((program, i) => {
const itemDay = {
progStart: parseStart($(schDayMonth), $(program)),
progStop: parseStop($(schDayMonth), schDayPrograms[i + 1] ?
$(schDayPrograms[i + 1]) : null),
progStop: parseStop(
$(schDayMonth),
schDayPrograms[i + 1] ? $(schDayPrograms[i + 1]) : null
),
progTitle: parseTitle($(program)),
progDesc: parseDescription($(program))
}
@ -91,7 +93,9 @@ function parseStop(schDayMonth, itemNext) {
)
} else {
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',
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())
for (let page of pages) {
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') },
headers: {
'X-Requested-With': 'XMLHttpRequest'
@ -65,7 +65,7 @@ module.exports = {
async function getTotalPageCount() {
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') },
headers: {
'X-Requested-With': 'XMLHttpRequest'

View file

@ -43,7 +43,7 @@ module.exports = {
const pages = Array.from(Array(totalPages).keys())
for (let page of pages) {
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') },
headers: {
'X-Requested-With': 'XMLHttpRequest'
@ -67,7 +67,7 @@ module.exports = {
async function getTotalPageCount() {
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') },
headers: {
'X-Requested-With': 'XMLHttpRequest'
@ -84,8 +84,8 @@ function parseContent(content, channel) {
let data
try {
data = JSON.parse(content)
} catch (error) {
console.log(error)
} catch {
return []
}
if (!data || !data.channels || !data.channels.length) return null

View file

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

View file

@ -9,7 +9,7 @@ dayjs.extend(customParseFormat)
const headers = {
'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 = {

View file

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

View file

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

View file

@ -17,16 +17,18 @@ const channel = {
xmltv_id: 'BBCOneLondon.uk'
}
axios.get.mockImplementation((url, opts) => {
axios.get.mockImplementation(url => {
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({
data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html'))
})
}
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({
data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html'))
@ -57,7 +59,8 @@ it('can parse response', async () => {
title: 'Captain Phillips',
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',
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']
})
expect(results[1]).toMatchObject({
@ -67,7 +70,8 @@ it('can parse response', async () => {
subTitle: 'Past and Pressure Season 6, Episode 5',
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',
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'],
season: 6,
episode: 5

View file

@ -1,73 +1,80 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'neo.io',
timezone: 'Europe/Ljubljana',
days: 5,
url({ date, channel }) { return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' },
request: {
method: 'POST',
headers: {
'Host': 'stargate.telekom.si',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/json',
'X-AppLayout': '1',
'x-language': 'sl',
'Origin': 'https://neo.io',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1',
'Connection': 'keep-alive'
},
data({ channel, date }) {
const todayEpoch = date.startOf('day').unix();
const nextDayEpoch = date.add(1, 'day').startOf('day').unix();
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 => {
const start = dayjs.unix(show.show_start).utc();
const stop = dayjs.unix(show.show_end).utc();
const programData = {
title: show.title,
description: show.summary || 'No description available',
start: start.toISOString(),
stop: stop.toISOString(),
thumbnail: show.thumbnail
}
programs.push(programData)
})
return programs
},
async channels() {
const response = await axios.post('https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList', JSON.stringify({ includeRadioStations: true }), {
headers: this.request.headers
});
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)
}))
}
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'neo.io',
timezone: 'Europe/Ljubljana',
days: 5,
url() {
return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
},
request: {
method: 'POST',
headers: {
Host: 'stargate.telekom.si',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/json',
'X-AppLayout': '1',
'x-language': 'sl',
Origin: 'https://neo.io',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1',
Connection: 'keep-alive'
},
data({ channel, date }) {
const todayEpoch = date.startOf('day').unix()
const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
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 => {
const start = dayjs.unix(show.show_start).utc()
const stop = dayjs.unix(show.show_end).utc()
const programData = {
title: show.title,
description: show.summary || 'No description available',
start: start.toISOString(),
stop: stop.toISOString(),
thumbnail: show.thumbnail
}
programs.push(programData)
})
return programs
},
async channels() {
const response = await axios.post(
'https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList',
JSON.stringify({ includeRadioStations: true }),
{
headers: this.request.headers
}
)
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'tv-slo-1',
xmltv_id: 'TVSLO1.si'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData')
})
it('can parse response', () => {
const content = `
{
"shows": [
{
"title": "Napovedujemo",
"show_start": 1735185900,
"show_end": 1735192200,
"timestamp": "5:05 - 6:50",
"show_id": "CUP_IECOM_SLO1_10004660",
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg",
"is_adult": false,
"friendly_id": "napovedujemo_db48",
"pg": "",
"genres": [
"napovednik"
],
"year": 0,
"summary": "Vabilo k ogledu naših oddaj.",
"categories": "Ostalo",
"stb_only": false,
"is_live": false,
"original_title": "Napovedujemo"
},
{
"title": "S0E0 - Hrabri zajčki: Prvi sneg",
"show_start": 1735192200,
"show_end": 1735192800,
"timestamp": "6:50 - 7:00",
"show_id": "CUP_IECOM_SLO1_79637910",
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg",
"is_adult": false,
"friendly_id": "hrabri_zajcki_prvi_sneg_1619",
"pg": "",
"genres": [
"risanka"
],
"year": 2020,
"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",
"stb_only": false,
"is_live": false,
"original_title": "S0E0 - Brave Bunnies"
},
{
"title": "Dobro jutro",
"show_start": 1735192800,
"show_end": 1735203900,
"timestamp": "7:00 - 10:05",
"show_id": "CUP_IECOM_SLO1_79637911",
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg",
"is_adult": false,
"friendly_id": "dobro_jutro_2f10",
"pg": "",
"genres": [
"zabavna oddaja"
],
"year": 2024,
"summary": "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.",
"categories": "Razvedrilni program",
"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
return p
})
expect(result).toMatchObject([
{
title: "Napovedujemo",
description: "Vabilo k ogledu naših oddaj.",
start: "2024-12-26T04:05:00.000Z",
stop: "2024-12-26T05:50:00.000Z",
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg"
},
{
title: "S0E0 - Hrabri zajčki: Prvi sneg",
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.",
start: "2024-12-26T05:50: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"
},
{
title: "Dobro jutro",
description: "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.",
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":[]}'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./neo.io.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'tv-slo-1',
xmltv_id: 'TVSLO1.si'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
)
})
it('can parse response', () => {
const content = `
{
"shows": [
{
"title": "Napovedujemo",
"show_start": 1735185900,
"show_end": 1735192200,
"timestamp": "5:05 - 6:50",
"show_id": "CUP_IECOM_SLO1_10004660",
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg",
"is_adult": false,
"friendly_id": "napovedujemo_db48",
"pg": "",
"genres": [
"napovednik"
],
"year": 0,
"summary": "Vabilo k ogledu naših oddaj.",
"categories": "Ostalo",
"stb_only": false,
"is_live": false,
"original_title": "Napovedujemo"
},
{
"title": "S0E0 - Hrabri zajčki: Prvi sneg",
"show_start": 1735192200,
"show_end": 1735192800,
"timestamp": "6:50 - 7:00",
"show_id": "CUP_IECOM_SLO1_79637910",
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg",
"is_adult": false,
"friendly_id": "hrabri_zajcki_prvi_sneg_1619",
"pg": "",
"genres": [
"risanka"
],
"year": 2020,
"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",
"stb_only": false,
"is_live": false,
"original_title": "S0E0 - Brave Bunnies"
},
{
"title": "Dobro jutro",
"show_start": 1735192800,
"show_end": 1735203900,
"timestamp": "7:00 - 10:05",
"show_id": "CUP_IECOM_SLO1_79637911",
"thumbnail": "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg",
"is_adult": false,
"friendly_id": "dobro_jutro_2f10",
"pg": "",
"genres": [
"zabavna oddaja"
],
"year": 2024,
"summary": "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.",
"categories": "Razvedrilni program",
"stb_only": false,
"is_live": false,
"original_title": "Dobro jutro"
}
]
}`
const result = parser({ content, channel })
expect(result).toMatchObject([
{
title: 'Napovedujemo',
description: 'Vabilo k ogledu naših oddaj.',
start: '2024-12-26T04:05:00.000Z',
stop: '2024-12-26T05:50:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg'
},
{
title: 'S0E0 - Hrabri zajčki: Prvi sneg',
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.',
start: '2024-12-26T05:50: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'
},
{
title: 'Dobro jutro',
description:
'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.',
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":[]}'
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

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

View file

@ -1,68 +1,68 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const headers = {
'X-Apikey': 'xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI',
'X-Core-Appversion': '2.14.0.1',
'X-Core-Contentratinglimit': '0',
'X-Core-Deviceid': '',
'X-Core-Devicetype': 'web',
Origin: 'https://nostv.pt',
'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'
}
module.exports = {
site: 'nostv.pt',
days: 2,
url({ channel, date }) {
return `https://tyr-prod.apigee.net/nostv/ott/schedule/range/contents/guest?channels=${
channel.site_id
}&minDate=${date.format('YYYY-MM-DD')}T00:00:00Z&maxDate=${date.format(
'YYYY-MM-DD'
)}T23:59:59Z&isDateInclusive=true&client_id=${headers['X-Apikey']}`
},
request: { headers },
parser({ content }) {
const programs = []
if (content) {
const items = Array.isArray(content) ? content : JSON.parse(content)
items.forEach(item => {
programs.push({
title: item.Metadata?.Title,
sub_title: item.Metadata?.SubTitle ? item.Metadata?.SubTitle : null,
description: item.Metadata?.Description,
season: item.Metadata?.Season,
episode: item.Metadata?.Episode,
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']}`
: null,
start: dayjs.utc(item.UtcDateTimeStart),
stop: dayjs.utc(item.UtcDateTimeEnd)
})
})
}
return programs
},
async channels() {
const result = await axios
.get(
`https://tyr-prod.apigee.net/nostv/ott/channels/guest?client_id=${headers['X-Apikey']}`,
{ headers }
)
.then(r => r.data)
.catch(console.error)
return result.map(item => {
return {
lang: 'pt',
site_id: item.ServiceId,
name: item.Name
}
})
}
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const headers = {
'X-Apikey': 'xe1dgrShwdR1DVOKGmsj8Ut4QLlGyOFI',
'X-Core-Appversion': '2.14.0.1',
'X-Core-Contentratinglimit': '0',
'X-Core-Deviceid': '',
'X-Core-Devicetype': 'web',
Origin: 'https://nostv.pt',
'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'
}
module.exports = {
site: 'nostv.pt',
days: 2,
url({ channel, date }) {
return `https://tyr-prod.apigee.net/nostv/ott/schedule/range/contents/guest?channels=${
channel.site_id
}&minDate=${date.format('YYYY-MM-DD')}T00:00:00Z&maxDate=${date.format(
'YYYY-MM-DD'
)}T23:59:59Z&isDateInclusive=true&client_id=${headers['X-Apikey']}`
},
request: { headers },
parser({ content }) {
const programs = []
if (content) {
const items = Array.isArray(content) ? content : JSON.parse(content)
items.forEach(item => {
programs.push({
title: item.Metadata?.Title,
sub_title: item.Metadata?.SubTitle ? item.Metadata?.SubTitle : null,
description: item.Metadata?.Description,
season: item.Metadata?.Season,
episode: item.Metadata?.Episode,
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']}`
: null,
start: dayjs.utc(item.UtcDateTimeStart),
stop: dayjs.utc(item.UtcDateTimeEnd)
})
})
}
return programs
},
async channels() {
const result = await axios
.get(
`https://tyr-prod.apigee.net/nostv/ott/channels/guest?client_id=${headers['X-Apikey']}`,
{ headers }
)
.then(r => r.data)
.catch(console.error)
return result.map(item => {
return {
lang: 'pt',
site_id: item.ServiceId,
name: item.Name
}
})
}
}

View file

@ -1,51 +1,51 @@
const { parser, url } = require('./nostv.pt.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-12-11').startOf('d')
const channel = {
site_id: '510',
xmltv_id: 'SPlus.pt'
}
it('can generate valid url', () => {
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'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-12-11T16:30:00.000Z',
stop: '2023-12-11T17:00:00.000Z',
title: 'Village Vets',
description:
'A história de dois melhores amigos veterinários e o seu extraordinário trabalho na Austrália.',
season: 1,
episode: 12,
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'
})
})
it('can handle empty guide', async () => {
const results = await parser({
date,
content: '[]'
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./nostv.pt.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-12-11').startOf('d')
const channel = {
site_id: '510',
xmltv_id: 'SPlus.pt'
}
it('can generate valid url', () => {
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'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-12-11T16:30:00.000Z',
stop: '2023-12-11T17:00:00.000Z',
title: 'Village Vets',
description:
'A história de dois melhores amigos veterinários e o seu extraordinário trabalho na Austrália.',
season: 1,
episode: 12,
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'
})
})
it('can handle empty guide', async () => {
const results = await parser({
date,
content: '[]'
})
expect(results).toMatchObject([])
})

View file

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

View file

@ -1,81 +1,81 @@
const parser = require('epg-parser')
module.exports = {
site: 'nzxmltv.com',
days: 2,
request: {
cache: {
ttl: 3600000 // 1 hour
},
maxContentLength: 104857600 // 100 MB
},
url({ channel }) {
const [path] = channel.site_id.split('#')
return `https://nzxmltv.com/${path}.xml`
},
parser({ content, channel, date }) {
const programs = []
parseItems(content, channel, date).forEach(item => {
const program = {
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
icon: item.icon?.[0],
start: item.start,
stop: item.stop
}
if (item.episodeNum) {
item.episodeNum.forEach(ep => {
if (ep.system === 'xmltv_ns') {
const [season, episode, _] = ep.value.split('.')
program.season = parseInt(season) + 1
program.episode = parseInt(episode) + 1
return true
}
})
}
programs.push(program)
})
return programs
},
async channels({ provider }) {
const axios = require('axios')
const cheerio = require('cheerio')
const providers = {
freeview: 'xmltv/guide',
sky: 'sky/guide',
redbull: 'iptv/redbull',
pluto: 'iptv/plutotv'
}
const channels = []
const path = providers[provider]
const xml = await axios
.get(`https://nzxmltv.com/${path}.xml`)
.then(r => r.data)
.catch(console.error)
const $ = cheerio.load(xml)
$('tv channel').each((i, el) => {
const disp = $(el).find('display-name')
const channelId = $(el).attr('id')
channels.push({
lang: disp.attr('lang').substr(0, 2),
site_id: `${path}#${channelId}`,
name: disp.text().trim()
})
})
return channels
}
}
function parseItems(content, channel, date) {
const { programs } = parser.parse(content)
const [, channelId] = channel.site_id.split('#')
return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day'))
}
const parser = require('epg-parser')
module.exports = {
site: 'nzxmltv.com',
days: 2,
request: {
cache: {
ttl: 3600000 // 1 hour
},
maxContentLength: 104857600 // 100 MB
},
url({ channel }) {
const [path] = channel.site_id.split('#')
return `https://nzxmltv.com/${path}.xml`
},
parser({ content, channel, date }) {
const programs = []
parseItems(content, channel, date).forEach(item => {
const program = {
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
icon: item.icon?.[0],
start: item.start,
stop: item.stop
}
if (item.episodeNum) {
item.episodeNum.forEach(ep => {
if (ep.system === 'xmltv_ns') {
const [season, episode] = ep.value.split('.')
program.season = parseInt(season) + 1
program.episode = parseInt(episode) + 1
return true
}
})
}
programs.push(program)
})
return programs
},
async channels({ provider }) {
const axios = require('axios')
const cheerio = require('cheerio')
const providers = {
freeview: 'xmltv/guide',
sky: 'sky/guide',
redbull: 'iptv/redbull',
pluto: 'iptv/plutotv'
}
const channels = []
const path = providers[provider]
const xml = await axios
.get(`https://nzxmltv.com/${path}.xml`)
.then(r => r.data)
.catch(console.error)
const $ = cheerio.load(xml)
$('tv channel').each((i, el) => {
const disp = $(el).find('display-name')
const channelId = $(el).attr('id')
channels.push({
lang: disp.attr('lang').substr(0, 2),
site_id: `${path}#${channelId}`,
name: disp.text().trim()
})
})
return channels
}
}
function parseItems(content, channel, date) {
const { programs } = parser.parse(content)
const [, channelId] = channel.site_id.split('#')
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 fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-11-21').startOf('d')
const channel = {
site_id: 'xmltv/guide#1',
xmltv_id: 'TVNZ1.nz'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://nzxmltv.com/xmltv/guide.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
const results = parser({ content, channel, date })
expect(results[0]).toMatchObject({
start: '2023-11-21T10:30:00.000Z',
stop: '2023-11-21T11:25:00.000Z',
title: 'Sunday',
description:
'On Sunday, an unmissable show with stories about divorce, weight loss, and the incomprehensible devastation of Gaza.',
season: 2023,
episode: 37,
icon: 'https://www.thetvdb.com/banners/posters/5dbebff2986f2.jpg'
})
})
it('can handle empty guide', () => {
const result = parser({ content: '', channel, date })
expect(result).toMatchObject([])
})
const { parser, url } = require('./nzxmltv.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-11-21').startOf('d')
const channel = {
site_id: 'xmltv/guide#1',
xmltv_id: 'TVNZ1.nz'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://nzxmltv.com/xmltv/guide.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
const results = parser({ content, channel, date })
expect(results[0]).toMatchObject({
start: '2023-11-21T10:30:00.000Z',
stop: '2023-11-21T11:25:00.000Z',
title: 'Sunday',
description:
'On Sunday, an unmissable show with stories about divorce, weight loss, and the incomprehensible devastation of Gaza.',
season: 2023,
episode: 37,
icon: 'https://www.thetvdb.com/banners/posters/5dbebff2986f2.jpg'
})
})
it('can handle empty guide', () => {
const result = parser({ content: '', channel, date })
expect(result).toMatchObject([])
})

View file

@ -132,7 +132,7 @@ module.exports = {
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
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')
if (!url) return
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)

View file

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

View file

@ -1,49 +1,54 @@
const { parser, url } = require('./orangetv.orange.es.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const path = require('path')
const fs = require('fs')
const date = dayjs.utc('2024-12-01', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1010',
xmltv_id: 'La1.es'
}
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`)
})
it('can parse response', async () => {
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()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(4)
var sampleResult = results[0];
expect(sampleResult).toMatchObject({
start: '2024-11-30T22:36:51.000Z',
stop: '2024-11-30T23:57:25.000Z',
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.',
title: 'Loco de amor'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '{}'
})
expect(result).toMatchObject({})
})
const { parser, url } = require('./orangetv.orange.es.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const path = require('path')
const fs = require('fs')
const date = dayjs.utc('2024-12-01', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1010',
xmltv_id: 'La1.es'
}
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`
)
})
it('can parse response', async () => {
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()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(4)
var sampleResult = results[0]
expect(sampleResult).toMatchObject({
start: '2024-11-30T22:36:51.000Z',
stop: '2024-11-30T23:57:25.000Z',
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.',
title: 'Loco de amor'
})
})
it('can handle empty guide', () => {
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(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 tz = 'Asia/Dubai'
@ -13,11 +18,9 @@ module.exports = {
site: 'osn.com',
days: 2,
url({ channel, date }) {
return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${
encodeURIComponent(date.format('MM/DD/YYYY'))
}&co=${country}&ch=${
channel.site_id
}&mo=false&hr=0`
return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${encodeURIComponent(
date.format('MM/DD/YYYY')
)}&co=${country}&ch=${channel.site_id}&mo=false&hr=0`
},
request: {
headers({ channel }) {
@ -46,7 +49,9 @@ module.exports = {
const axios = require('axios')
for (const pkg of Object.values(packages)) {
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)
.catch(console.error)

View file

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

View file

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

View file

@ -1,76 +1,69 @@
jest.mock('./pickx.be.config.js', () => {
const originalModule = jest.requireActual('./pickx.be.config.js')
return {
...originalModule,
fetchApiVersion: jest.fn(() => Promise.resolve())
}
})
const {
parser,
url,
request,
fetchApiVersion,
setApiVersion,
getApiVersion
} = require('./pickx.be.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2023-12-13').startOf('d')
const channel = {
lang: 'fr',
site_id: 'UID0118',
xmltv_id: 'Vedia.be'
}
beforeEach(() => {
setApiVersion('mockedApiVersion')
})
it('can generate valid url', async () => {
const generatedUrl = await url({ channel, date })
expect(generatedUrl).toBe(
`https://px-epg.azureedge.net/airings/mockedApiVersion/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels`
)
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
Origin: 'https://www.pickx.be',
Referer: 'https://www.pickx.be/'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result[0]).toMatchObject({
start: '2023-12-12T23:55:00.000Z',
stop: '2023-12-13T00:15:00.000Z',
title: 'Le 22h30',
description: 'Le journal de vivre ici.',
category: 'Info',
image:
'https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/250_250_4B990CC58066A7B2A660AFA0BDDE5C41.jpg'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: ''
})
expect(result).toMatchObject([])
})
jest.mock('./pickx.be.config.js', () => {
const originalModule = jest.requireActual('./pickx.be.config.js')
return {
...originalModule,
fetchApiVersion: jest.fn(() => Promise.resolve())
}
})
const { parser, url, request, setApiVersion } = require('./pickx.be.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2023-12-13').startOf('d')
const channel = {
lang: 'fr',
site_id: 'UID0118',
xmltv_id: 'Vedia.be'
}
beforeEach(() => {
setApiVersion('mockedApiVersion')
})
it('can generate valid url', async () => {
const generatedUrl = await url({ channel, date })
expect(generatedUrl).toBe(
'https://px-epg.azureedge.net/airings/mockedApiVersion/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels'
)
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
Origin: 'https://www.pickx.be',
Referer: 'https://www.pickx.be/'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result[0]).toMatchObject({
start: '2023-12-12T23:55:00.000Z',
stop: '2023-12-13T00:15:00.000Z',
title: 'Le 22h30',
description: 'Le journal de vivre ici.',
category: 'Info',
image:
'https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/250_250_4B990CC58066A7B2A660AFA0BDDE5C41.jpg'
})
})
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'player.ee.co.uk',
days: 2,
url({ date, channel, hour = 0 }) {
return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${
encodeURIComponent(channel.site_id)
}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2,'0')}Z/PT12H`
},
request: {
headers: {
Referer: 'https://player.ee.co.uk/'
}
},
async parser({ content, channel, date }) {
const programs = []
if (content) {
const schedule = JSON.parse(content)
// fetch next 12 hours schedule
const { url, request } = module.exports
const nextSchedule = await axios
.get(url({ channel, date, hour: 12 }), { headers: request.headers })
.then(response => response.data)
.catch(console.error)
if (schedule?.items) {
// merge schedules
if (nextSchedule?.items) {
schedule.items.push(...nextSchedule.items)
}
schedule.items.forEach(item => {
let season, episode
const start = dayjs.utc(item.publishedStartTime)
const stop = start.add(item.publishedDuration, 's')
const description = item.synopsis
if (description) {
const matches = description.trim().match(/\(?S(\d+)[\/\s]Ep(\d+)\)?/)
if (matches) {
if (matches[1]) {
season = parseInt(matches[1])
}
if (matches[2]) {
episode = parseInt(matches[2])
}
}
}
programs.push({
title: item.title,
description,
season,
episode,
start,
stop
})
})
}
}
return programs
},
async channels() {
const token =
'eyJkaXNjb3ZlcnlVc2VyR3JvdXBzIjpbIkFMTFVTRVJTIiwiYWxsIiwiaHR0cDovL3JlZmRhd' +
'GEueW91dmlldy5jb20vbXBlZzdjcy9Zb3VWaWV3QXBwbGljYXRpb25QbGF5ZXJDUy8yMDIxLT' +
'A5LTEwI2FuZHJvaWRfcnVudGltZS1wcm9maWxlMSIsInRhZzpidC5jb20sMjAxOC0wNy0xMTp' +
'1c2VyZ3JvdXAjR0JSLWJ0X25vd1RWX211bHRpY2FzdCIsInRhZzpidC5jb20sMjAyMS0xMC0y' +
'NTp1c2VyZ3JvdXAjR0JSLWJ0X2V1cm9zcG9ydCJdLCJyZWdpb25zIjpbIkFMTFJFR0lPTlMiL' +
'CJHQlIiLCJHQlItRU5HIiwiR0JSLUVORy1sb25kb24iLCJhbGwiXSwic3Vic2V0IjoiMy41Lj' +
'EvYW5kcm9pZF9ydW50aW1lLXByb2ZpbGUxL0JST0FEQ0FTVF9JUC9HQlItYnRfYnJvYWRiYW5' +
'kIiwic3Vic2V0cyI6WyIvLy8iLCIvL0JST0FEQ0FTVF9JUC8iLCIzLjUvLy8iXX0='
const extensions = [
'LinearCategoriesExtension',
'LogicalChannelNumberExtension',
'BTSubscriptionCodesExtension'
]
const result = await axios
.get(`https://api.youview.tv/metadata/linear/v2/linear-services`, {
params: {
contentTargetingToken: token,
extensions: extensions.join(',')
},
headers: module.exports.request.headers
})
.then(response => response.data)
.catch(console.error)
return result?.items
.filter(channel => channel.contentTypes.indexOf('tv') >= 0)
.map(channel => {
return {
lang: 'en',
site_id: channel.serviceLocator,
name: channel.fullName
}
}) || []
}
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'player.ee.co.uk',
days: 2,
url({ date, channel, hour = 0 }) {
return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${encodeURIComponent(
channel.site_id
)}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2, '0')}Z/PT12H`
},
request: {
headers: {
Referer: 'https://player.ee.co.uk/'
}
},
async parser({ content, channel, date }) {
const programs = []
if (content) {
const schedule = JSON.parse(content)
// fetch next 12 hours schedule
const { url, request } = module.exports
const nextSchedule = await axios
.get(url({ channel, date, hour: 12 }), { headers: request.headers })
.then(response => response.data)
.catch(console.error)
if (schedule?.items) {
// merge schedules
if (nextSchedule?.items) {
schedule.items.push(...nextSchedule.items)
}
schedule.items.forEach(item => {
let season, episode
const start = dayjs.utc(item.publishedStartTime)
const stop = start.add(item.publishedDuration, 's')
const description = item.synopsis
if (description) {
const matches = description.trim().match(/\(?S(\d+)[/\s]Ep(\d+)\)?/)
if (matches) {
if (matches[1]) {
season = parseInt(matches[1])
}
if (matches[2]) {
episode = parseInt(matches[2])
}
}
}
programs.push({
title: item.title,
description,
season,
episode,
start,
stop
})
})
}
}
return programs
},
async channels() {
const token =
'eyJkaXNjb3ZlcnlVc2VyR3JvdXBzIjpbIkFMTFVTRVJTIiwiYWxsIiwiaHR0cDovL3JlZmRhd' +
'GEueW91dmlldy5jb20vbXBlZzdjcy9Zb3VWaWV3QXBwbGljYXRpb25QbGF5ZXJDUy8yMDIxLT' +
'A5LTEwI2FuZHJvaWRfcnVudGltZS1wcm9maWxlMSIsInRhZzpidC5jb20sMjAxOC0wNy0xMTp' +
'1c2VyZ3JvdXAjR0JSLWJ0X25vd1RWX211bHRpY2FzdCIsInRhZzpidC5jb20sMjAyMS0xMC0y' +
'NTp1c2VyZ3JvdXAjR0JSLWJ0X2V1cm9zcG9ydCJdLCJyZWdpb25zIjpbIkFMTFJFR0lPTlMiL' +
'CJHQlIiLCJHQlItRU5HIiwiR0JSLUVORy1sb25kb24iLCJhbGwiXSwic3Vic2V0IjoiMy41Lj' +
'EvYW5kcm9pZF9ydW50aW1lLXByb2ZpbGUxL0JST0FEQ0FTVF9JUC9HQlItYnRfYnJvYWRiYW5' +
'kIiwic3Vic2V0cyI6WyIvLy8iLCIvL0JST0FEQ0FTVF9JUC8iLCIzLjUvLy8iXX0='
const extensions = [
'LinearCategoriesExtension',
'LogicalChannelNumberExtension',
'BTSubscriptionCodesExtension'
]
const result = await axios
.get('https://api.youview.tv/metadata/linear/v2/linear-services', {
params: {
contentTargetingToken: token,
extensions: extensions.join(',')
},
headers: module.exports.request.headers
})
.then(response => response.data)
.catch(console.error)
return (
result?.items
.filter(channel => channel.contentTypes.indexOf('tv') >= 0)
.map(channel => {
return {
lang: 'en',
site_id: channel.serviceLocator,
name: channel.fullName
}
}) || []
)
}
}

View file

@ -1,72 +1,74 @@
const { parser, url } = require('./player.ee.co.uk.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2023-12-13').startOf('d')
const channel = {
site_id: 'dvb://233a..6d60',
xmltv_id: 'HGTV.uk'
}
axios.get.mockImplementation((url, opts) => {
if (url === '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', () => {
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 => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Bargain Mansions',
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',
season: 4,
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',
season: 2,
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,
date,
content: ''
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./player.ee.co.uk.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2023-12-13').startOf('d')
const channel = {
site_id: 'dvb://233a..6d60',
xmltv_id: 'HGTV.uk'
}
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'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json')))
})
}
return Promise.resolve({ data: '' })
})
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 => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Bargain Mansions',
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',
season: 4,
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',
season: 2,
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,
date,
content: ''
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

@ -33,14 +33,17 @@ it('can parse response', () => {
start: '2024-12-28T00:21:00.000Z',
stop: '2024-12-28T00:48:00.000Z',
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',
episode: 80,
season: 2,
actors: ["Isabel Martion (Naruto Uzumaki)",
"Christine Byrd (Sakura Haruno)",
"Victor Ugarte (Sasuke Uchiha)",
"Alfonso Obreg (Kakashi Hatake)"],
actors: [
'Isabel Martion (Naruto Uzumaki)',
'Christine Byrd (Sakura Haruno)',
'Victor Ugarte (Sasuke Uchiha)',
'Alfonso Obreg (Kakashi Hatake)'
],
categories: ['Anime', 'Anime Action & Adventure'],
rating: 'TV-14',
date: '2004-04-21T00:00:00.000Z',

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<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"?>
<channels>
<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
},
async channels({ country, lang }) {
async channels() {
const axios = require('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)
.catch(console.log)

View file

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

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