mirror of
https://github.com/iptv-org/epg
synced 2026-09-11 22:36:50 -04:00
Merge branch 'master' into pr/2794
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Enforce the usage of CRLF in GitHub Actions per ESLint configuration.
|
||||
* text eol=crlf
|
||||
@@ -18,22 +18,23 @@ jobs:
|
||||
- name: changed files
|
||||
id: files
|
||||
run: |
|
||||
git fetch origin master:master
|
||||
JS_ANY_CHANGED=false
|
||||
JS_ALL_CHANGED_FILES=$(git diff --name-only tests/**/*.js tests/**/*.ts scripts/**/*.js scripts/**/*.mts scripts/**/*.ts sites/**/*.js sites/**/*.ts | tr '\n' ' ')
|
||||
JS_ALL_CHANGED_FILES=$(git diff --name-only master -- tests/**/*.js tests/**/*.ts scripts/**/*.js scripts/**/*.mts scripts/**/*.ts sites/**/*.js sites/**/*.ts | tr '\n' ' ')
|
||||
if [ -n "${JS_ALL_CHANGED_FILES}" ]; then
|
||||
JS_ANY_CHANGED=true
|
||||
fi
|
||||
echo "js_all_changed_files=$JS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||
echo "js_any_changed=$JS_ANY_CHANGED" >> "$GITHUB_OUTPUT"
|
||||
CHANNELS_ANY_CHANGED=false
|
||||
CHANNELS_ALL_CHANGED_FILES=$(git diff --name-only sites/**/*.channels.xml | tr '\n' ' ')
|
||||
CHANNELS_ALL_CHANGED_FILES=$(git diff --name-only master -- sites/**/*.channels.xml | tr '\n' ' ')
|
||||
if [ -n "${CHANNELS_ALL_CHANGED_FILES}" ]; then
|
||||
CHANNELS_ANY_CHANGED=true
|
||||
fi
|
||||
echo "channels_all_changed_files=$CHANNELS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||
echo "channels_any_changed=$CHANNELS_ANY_CHANGED" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/setup-node@v4
|
||||
if: ${{ !env.ACT && (steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true') }}
|
||||
if: ${{ steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true' }}
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
@@ -7,14 +7,14 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: tibdex/github-app-token@v1.8.2
|
||||
if: ${{ !env.ACT }}
|
||||
id: create-app-token
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
token: ${{ steps.create-app-token.outputs.token }}
|
||||
@@ -22,8 +22,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "iptv-bot[bot]"
|
||||
git config user.email "84861620+iptv-bot[bot]@users.noreply.github.com"
|
||||
- uses: actions/setup-node@v3
|
||||
if: ${{ !env.ACT }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
@@ -65,6 +65,7 @@ Options:
|
||||
--days <days> Number of days for which the program will be loaded (defaults to the value from the site config)
|
||||
--maxConnections <number> Number of concurrent requests (default: 1)
|
||||
--gzip Specifies whether or not to create a compressed version of the guide (default: false)
|
||||
--curl Display each request as CURL (default: false)
|
||||
```
|
||||
|
||||
### Parallel downloading
|
||||
@@ -97,19 +98,15 @@ npm run grab --- --channels=path/to/custom.channels.xml
|
||||
|
||||
### Run on schedule
|
||||
|
||||
To download the guide on a schedule, you can use the included process manager. Just run it with desire [cron expression](https://crontab.guru/) and the `grab` options:
|
||||
If you want to download guides on a schedule, you can use [cron](https://en.wikipedia.org/wiki/Cron) or any other task scheduler. Currently, we use a tool called `chronos` for this purpose.
|
||||
|
||||
To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/):
|
||||
|
||||
```sh
|
||||
npx pm2 start npm --no-autorestart --cron-restart="0 0,12 * * *" -- run grab --- --site=example.com
|
||||
npx chronos --execute="npm run grab --- --site=example.com" --pattern="0 0,12 * * *" --log
|
||||
```
|
||||
|
||||
To track the process, you can use the command:
|
||||
|
||||
```sh
|
||||
npx pm2 logs
|
||||
```
|
||||
|
||||
For more info go to [pm2](https://pm2.keymetrics.io/docs/usage/quick-start/) documentation.
|
||||
For more info go to [chronos](https://github.com/freearhey/chronos) documentation.
|
||||
|
||||
### Access the guide by URL
|
||||
|
||||
@@ -186,6 +183,7 @@ docker run \
|
||||
-e CRON_SCHEDULE="0 0,12 * * *" \
|
||||
-e MAX_CONNECTIONS=10 \
|
||||
-e GZIP=true \
|
||||
-e CURL=true \
|
||||
-e PROXY="socks5://127.0.0.1:1234" \
|
||||
-e DAYS=14 \
|
||||
-e TIMEOUT=5 \
|
||||
@@ -198,6 +196,7 @@ iptv-org/epg
|
||||
| CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") |
|
||||
| MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) |
|
||||
| GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) |
|
||||
| CURL | Display each request as CURL (default: false) |
|
||||
| PROXY | Use the specified proxy |
|
||||
| DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) |
|
||||
| TIMEOUT | Timeout for each request in milliseconds (default: 0) |
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td align="right">32</td><td align="right">30</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td align="right">157</td><td align="right">112</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/cosmotetv.gr">cosmotetv.gr</a></td><td align="right">108</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/ctc.ru">ctc.ru</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td align="right">174</td><td align="right">122</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/cyta.com.cy">cyta.com.cy</a></td><td align="right">116</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/dens.tv">dens.tv</a></td><td align="right">67</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -69,9 +70,10 @@
|
||||
<tr><td><a href="sites/guiadetv.com">guiadetv.com</a></td><td align="right">124</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/guida.tv">guida.tv</a></td><td align="right">88</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td align="right">168</td><td align="right">153</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/guidetnt.com">guidetnt.com</a></td><td align="right">69</td><td align="right">69</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td align="right">184</td><td align="right">172</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td align="right">3</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td align="right">6458</td><td align="right">1488</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td align="right">6458</td><td align="right">1489</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td align="right">4</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td align="right">142</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td align="right">130</td><td align="right">124</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -94,7 +96,7 @@
|
||||
<tr><td><a href="sites/meo.pt">meo.pt</a></td><td align="right">216</td><td align="right">192</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td align="right">102</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td align="right">25</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td align="right">2084</td><td align="right">620</td><td align="center">🟢</td><td>https://github.com/iptv-org/epg/issues/2785</td></tr>
|
||||
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td align="right">2084</td><td align="right">620</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td align="right">276</td><td align="right">223</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/moji.id">moji.id</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/mojmaxtv.hrvatskitelekom.hr">mojmaxtv.hrvatskitelekom.hr</a></td><td align="right">243</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -149,7 +151,7 @@
|
||||
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td align="right">155</td><td align="right">113</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td align="right">111</td><td align="right">93</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">559</td><td align="right">458</td><td align="center">🟢</td><td>https://github.com/iptv-org/epg/issues/2763</td></tr>
|
||||
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">559</td><td align="right">458</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2763</td></tr>
|
||||
<tr><td><a href="sites/sky.de">sky.de</a></td><td align="right">75</td><td align="right">75</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td align="right">251</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td align="right">137</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -162,6 +164,7 @@
|
||||
<tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td align="right">204</td><td align="right">163</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td align="right">39</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tataplay.com">tataplay.com</a></td><td align="right">785</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/telebilbao.es">telebilbao.es</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/teleboy.ch">teleboy.ch</a></td><td align="right">325</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td align="right">260</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -169,7 +172,7 @@
|
||||
<tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td align="right">66</td><td align="right">32</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td align="right">17</td><td align="right">15</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/thesportplus.com">thesportplus.com</a></td><td align="right">3</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tivie.id">tivie.id</a></td><td align="right">50</td><td align="right">49</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tivie.id">tivie.id</a></td><td align="right">45</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td align="right">69</td><td align="right">66</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td align="right">177</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -177,16 +180,16 @@
|
||||
<tr><td><a href="sites/tv-spored.siol.net">tv-spored.siol.net</a></td><td align="right">312</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td align="right">1030</td><td align="right">565</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td align="right">94</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td align="right">111</td><td align="right">93</td><td align="center">🟢</td><td>https://github.com/iptv-org/epg/issues/2779</td></tr>
|
||||
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td align="right">111</td><td align="right">93</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2779</td></tr>
|
||||
<tr><td><a href="sites/tv.lv">tv.lv</a></td><td align="right">137</td><td align="right">49</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td align="right">307</td><td align="right">228</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td align="right">664</td><td align="right">643</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td align="right">282</td><td align="right">40</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.nu">tv.nu</a></td><td align="right">199</td><td align="right">180</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td align="right">332</td><td align="right">242</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.sfr.fr">tv.sfr.fr</a></td><td align="right">503</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.sfr.fr">tv.sfr.fr</a></td><td align="right">489</td><td align="right">456</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td align="right">266</td><td align="right">74</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td align="right">97</td><td align="right">67</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td align="right">97</td><td align="right">67</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2803</td></tr>
|
||||
<tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td align="right">1072</td><td align="right">39</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv24.se">tv24.se</a></td><td align="right">326</td><td align="right">157</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td align="right">335</td><td align="right">254</td><td align="center">🟢</td><td></td></tr>
|
||||
@@ -208,11 +211,11 @@
|
||||
<tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td align="right">99</td><td align="right">67</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tvmustra.hu">tvmustra.hu</a></td><td align="right">188</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td align="right">19287</td><td align="right">2509</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td align="right">143</td><td align="right">134</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td align="right">150</td><td align="right">144</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td align="right">5836</td><td align="right">455</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td align="right">2299</td><td align="right">2255</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td align="right">47</td><td align="right">46</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td align="right">57</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td align="right">238</td><td align="right">195</td><td align="center">🟢</td><td></td></tr>
|
||||
<tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td align="right">250</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
||||
|
||||
+5
-3
@@ -1,4 +1,5 @@
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
import globals from 'globals'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import path from 'node:path'
|
||||
@@ -15,10 +16,11 @@ const compat = new FlatCompat({
|
||||
})
|
||||
|
||||
export default [
|
||||
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'),
|
||||
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/strict', 'plugin:@typescript-eslint/stylistic', 'prettier'),
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
@@ -36,7 +38,7 @@ export default [
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'linebreak-style': ['error', 'windows'],
|
||||
'@stylistic/linebreak-style': ['error', 'windows'],
|
||||
|
||||
quotes: [
|
||||
'error',
|
||||
|
||||
Generated
+7657
-4777
File diff suppressed because it is too large
Load Diff
+71
-61
@@ -3,24 +3,21 @@
|
||||
"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",
|
||||
"channels:lint": "npx tsx scripts/commands/channels/lint.mts",
|
||||
"channels:parse": "npx tsx scripts/commands/channels/parse.ts",
|
||||
"channels:edit": "npx tsx scripts/commands/channels/edit.ts",
|
||||
"channels:validate": "npx tsx scripts/commands/channels/validate.ts",
|
||||
"sites:init": "npx tsx scripts/commands/sites/init.ts",
|
||||
"sites:update": "npx tsx scripts/commands/sites/update.ts",
|
||||
"grab": "npx tsx scripts/commands/epg/grab.ts",
|
||||
"api:load": "tsx scripts/commands/api/load.ts",
|
||||
"api:generate": "tsx scripts/commands/api/generate.ts",
|
||||
"channels:lint": "tsx scripts/commands/channels/lint.mts",
|
||||
"channels:parse": "tsx scripts/commands/channels/parse.ts",
|
||||
"channels:edit": "tsx scripts/commands/channels/edit.ts",
|
||||
"channels:validate": "tsx scripts/commands/channels/validate.ts",
|
||||
"sites:init": "tsx scripts/commands/sites/init.ts",
|
||||
"sites:update": "tsx scripts/commands/sites/update.ts",
|
||||
"grab": "tsx scripts/commands/epg/grab.ts",
|
||||
"lint": "npx eslint \"{scripts,tests,sites}/**/*.{ts,mts,js}\"",
|
||||
"test": "run-script-os",
|
||||
"test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand",
|
||||
"test:default": "TZ=Pacific/Nauru npx jest --runInBand",
|
||||
"test": "cross-env TZ=Pacific/Nauru npx jest --runInBand",
|
||||
"postinstall": "skip-postinstall || npm run api:load",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"private": true,
|
||||
"author": "Arhey",
|
||||
"license": "UNLICENSED",
|
||||
"jest": {
|
||||
"setupFiles": [
|
||||
@@ -35,79 +32,92 @@
|
||||
],
|
||||
"testTimeout": 10000,
|
||||
"transformIgnorePatterns": [
|
||||
"<rootDir>/node_modules/(?!parse-duration/.*)"
|
||||
"<rootDir>/node_modules/(?!parse-duration/.*|@freearhey/core/.*|glob/.*|srcset/.*|balanced-match/.*|minimatch/.*)"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@alex_neo/jest-expect-message": "^1.0.5",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@freearhey/core": "^0.7.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@freearhey/chronos": "^0.0.1",
|
||||
"@freearhey/core": "^0.10.2",
|
||||
"@freearhey/search-js": "^0.1.2",
|
||||
"@ntlab/sfetch": "^1.2.0",
|
||||
"@octokit/core": "^6.1.3",
|
||||
"@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",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/node-cleanup": "^2.1.2",
|
||||
"@types/numeral": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.20.0",
|
||||
"axios": "^1.5.1",
|
||||
"axios-cookiejar-support": "^5.0.4",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"@octokit/core": "^7.0.3",
|
||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.2.2",
|
||||
"@swc/core": "^1.13.2",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@types/cli-progress": "^3.11.6",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/inquirer": "^9.0.8",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/langs": "^2.0.5",
|
||||
"@types/lodash.orderby": "^4.6.9",
|
||||
"@types/lodash.sortby": "^4.7.9",
|
||||
"@types/lodash.startcase": "^4.4.9",
|
||||
"@types/lodash.uniqby": "^4.7.9",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/node-cleanup": "^2.1.5",
|
||||
"@types/numeral": "^2.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-cookiejar-support": "^6.0.4",
|
||||
"chalk": "^5.4.1",
|
||||
"cheerio": "^1.1.2",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^8.2.0",
|
||||
"consola": "^3.2.3",
|
||||
"csv-parser": "^3.0.0",
|
||||
"commander": "^14.0.0",
|
||||
"consola": "^3.4.2",
|
||||
"cross-env": "^10.0.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"cwait": "^1.1.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"epg-grabber": "^0.38.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"epg-grabber": "^0.41.0",
|
||||
"epg-parser": "^0.3.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"globals": "^15.14.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"form-data": "^4.0.4",
|
||||
"fs-extra": "^11.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"iconv-lite": "^0.4.24",
|
||||
"inquirer": "^12.5.0",
|
||||
"jest": "^29.7.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"inquirer": "^12.8.2",
|
||||
"jest": "^30.0.5",
|
||||
"jest-offline": "^1.0.1",
|
||||
"langs": "^2.0.0",
|
||||
"libxml2-wasm": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.3.0",
|
||||
"lodash.orderby": "^4.6.0",
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"luxon": "^3.7.1",
|
||||
"mockdate": "^3.0.5",
|
||||
"nedb-promises": "^6.0.3",
|
||||
"nedb-promises": "^6.2.3",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-gzip": "^1.1.2",
|
||||
"numeral": "^2.0.6",
|
||||
"pako": "^2.1.0",
|
||||
"parse-duration": "^2.1.3",
|
||||
"parse-duration": "^2.1.4",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pm2": "^5.4.3",
|
||||
"pm2": "^6.0.8",
|
||||
"readline": "^1.3.0",
|
||||
"run-script-os": "^1.1.6",
|
||||
"serve": "^14.2.4",
|
||||
"signale": "^1.4.0",
|
||||
"skip-postinstall": "^1.0.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"srcset": "^4.0.0",
|
||||
"srcset": "^5.0.1",
|
||||
"table2array": "^0.0.2",
|
||||
"tabletojson": "^2.0.7",
|
||||
"tough-cookie": "^5.0.0",
|
||||
"transliteration": "^2.2.0",
|
||||
"tsx": "^4.19.2",
|
||||
"unzipit": "^1.4.0",
|
||||
"wildcard-match": "^5.1.2"
|
||||
"tabletojson": "^4.1.6",
|
||||
"tough-cookie": "^5.1.2",
|
||||
"transliteration": "^2.3.5",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3",
|
||||
"unzipit": "^1.4.3",
|
||||
"uuid": "^11.1.0",
|
||||
"wildcard-match": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -1,3 +1,9 @@
|
||||
const grab = process.env.SITE
|
||||
? `npm run grab -- --site=${process.env.SITE} ${
|
||||
process.env.CLANG ? `--lang=${process.env.CLANG}` : ''
|
||||
} --output=public/guide.xml`
|
||||
: 'npm run grab -- --channels=channels.xml --output=public/guide.xml'
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
@@ -9,15 +15,10 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
name: 'grab',
|
||||
script: process.env.SITE
|
||||
? `npm run grab -- --site=${process.env.SITE} ${
|
||||
process.env.CLANG ? `--lang=${process.env.CLANG}` : ''
|
||||
} --output=public/guide.xml`
|
||||
: 'npm run grab -- --channels=channels.xml --output=public/guide.xml',
|
||||
cron_restart: process.env.CRON || null,
|
||||
script: `npx chronos -e "${grab}" -p "${process.env.CRON_SCHEDULE}" -l`,
|
||||
instances: 1,
|
||||
watch: false,
|
||||
autorestart: false
|
||||
autorestart: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
||||
import { ChannelsParser } from '../../core'
|
||||
import path from 'path'
|
||||
import { Logger, Collection, Storage } from '@freearhey/core'
|
||||
import { SITES_DIR, API_DIR } from '../../constants'
|
||||
import { GuideChannel } from '../../models'
|
||||
import { ChannelsParser } from '../../core'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
|
||||
type OutputItem = {
|
||||
channel: string | null
|
||||
feed: string | null
|
||||
site: string
|
||||
site_id: string
|
||||
site_name: string
|
||||
lang: string
|
||||
}
|
||||
import path from 'path'
|
||||
|
||||
async function main() {
|
||||
const logger = new Logger()
|
||||
@@ -20,32 +12,25 @@ async function main() {
|
||||
|
||||
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: epgGrabber.Channel): OutputItem => {
|
||||
const xmltv_id = channel.xmltv_id || ''
|
||||
const [channelId, feedId] = xmltv_id.split('@')
|
||||
|
||||
return {
|
||||
channel: channelId || null,
|
||||
feed: feedId || null,
|
||||
site: channel.site || '',
|
||||
site_id: channel.site_id || '',
|
||||
site_name: channel.name,
|
||||
lang: channel.lang || ''
|
||||
}
|
||||
const parser = new ChannelsParser({
|
||||
storage: sitesStorage
|
||||
})
|
||||
|
||||
const files: string[] = await sitesStorage.list('**/*.channels.xml')
|
||||
|
||||
const channels = new Collection()
|
||||
for (const filepath of files) {
|
||||
const channelList = await parser.parse(filepath)
|
||||
|
||||
channelList.channels.forEach((data: epgGrabber.Channel) => {
|
||||
channels.add(new GuideChannel(data))
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`found ${channels.count()} channel(s)`)
|
||||
|
||||
const output = channels.map((channel: GuideChannel) => channel.toJSON())
|
||||
|
||||
const apiStorage = new Storage(API_DIR)
|
||||
const outputFilename = 'guides.json'
|
||||
await apiStorage.save('guides.json', output.toJSON())
|
||||
|
||||
@@ -17,7 +17,8 @@ async function main() {
|
||||
loader.download('feeds.json'),
|
||||
loader.download('timezones.json'),
|
||||
loader.download('guides.json'),
|
||||
loader.download('streams.json')
|
||||
loader.download('streams.json'),
|
||||
loader.download('logos.json')
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||
import type { DataLoaderData } from '../../types/dataLoader'
|
||||
import { ChannelSearchableData } from '../../types/channel'
|
||||
import { Channel, ChannelList, Feed } from '../../models'
|
||||
import { DataProcessor, DataLoader } from '../../core'
|
||||
import { select, input } from '@inquirer/prompts'
|
||||
import { ChannelsParser, XML } from '../../core'
|
||||
import { Channel, Feed } from '../../models'
|
||||
import { ChannelsParser } from '../../core'
|
||||
import { DATA_DIR } from '../../constants'
|
||||
import nodeCleanup from 'node-cleanup'
|
||||
import sjs from '@freearhey/search-js'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
import { Command } from 'commander'
|
||||
import readline from 'readline'
|
||||
import sjs from '@freearhey/search-js'
|
||||
import { DataProcessor, DataLoader } from '../../core'
|
||||
import type { DataLoaderData } from '../../types/dataLoader'
|
||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
import { ChannelSearchableData } from '../../types/channel'
|
||||
|
||||
type ChoiceValue = { type: string; value?: Feed | Channel }
|
||||
type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean }
|
||||
interface ChoiceValue { type: string; value?: Feed | Channel }
|
||||
interface Choice { name: string; short?: string; value: ChoiceValue; default?: boolean }
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
readline
|
||||
@@ -34,11 +34,11 @@ program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(proc
|
||||
const filepath = program.args[0]
|
||||
const logger = new Logger()
|
||||
const storage = new Storage()
|
||||
let parsedChannels = new Collection()
|
||||
let channelList = new ChannelList({ channels: [] })
|
||||
|
||||
main(filepath)
|
||||
nodeCleanup(() => {
|
||||
save(filepath)
|
||||
save(filepath, channelList)
|
||||
})
|
||||
|
||||
export default async function main(filepath: string) {
|
||||
@@ -51,18 +51,18 @@ export default async function main(filepath: string) {
|
||||
const dataStorage = new Storage(DATA_DIR)
|
||||
const loader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await loader.load()
|
||||
const { feedsGroupedByChannelId, channels, channelsKeyById }: DataProcessorData =
|
||||
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
||||
processor.process(data)
|
||||
|
||||
logger.info('loading channels...')
|
||||
const parser = new ChannelsParser({ storage })
|
||||
parsedChannels = await parser.parse(filepath)
|
||||
const parsedChannelsWithoutId = parsedChannels.filter(
|
||||
channelList = await parser.parse(filepath)
|
||||
const parsedChannelsWithoutId = channelList.channels.filter(
|
||||
(channel: epgGrabber.Channel) => !channel.xmltv_id
|
||||
)
|
||||
|
||||
logger.info(
|
||||
`found ${parsedChannels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
||||
`found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
||||
)
|
||||
|
||||
logger.info('creating search index...')
|
||||
@@ -73,10 +73,10 @@ export default async function main(filepath: string) {
|
||||
|
||||
logger.info('starting...\n')
|
||||
|
||||
for (const parsedChannel of parsedChannelsWithoutId.all()) {
|
||||
for (const channel of parsedChannelsWithoutId.all()) {
|
||||
try {
|
||||
parsedChannel.xmltv_id = await selectChannel(
|
||||
parsedChannel,
|
||||
channel.xmltv_id = await selectChannel(
|
||||
channel,
|
||||
searchIndex,
|
||||
feedsGroupedByChannelId,
|
||||
channelsKeyById
|
||||
@@ -124,8 +124,8 @@ async function selectChannel(
|
||||
case 'channel': {
|
||||
const selectedChannel = selected.value
|
||||
if (!selectedChannel) return ''
|
||||
const selectedFeedId = await selectFeed(selectedChannel.id, feedsGroupedByChannelId)
|
||||
if (selectedFeedId === '-') return selectedChannel.id
|
||||
const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
|
||||
if (selectedFeedId === '-') return selectedChannel.id || ''
|
||||
return [selectedChannel.id, selectedFeedId].join('@')
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary
|
||||
case 'feed':
|
||||
const selectedFeed = selected.value
|
||||
if (!selectedFeed) return ''
|
||||
return selectedFeed.id
|
||||
return selectedFeed.id || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
@@ -205,10 +205,9 @@ function getFeedChoises(feeds: Collection): Choice[] {
|
||||
return choises
|
||||
}
|
||||
|
||||
function save(filepath: string) {
|
||||
function save(filepath: string, channelList: ChannelList) {
|
||||
if (!storage.existsSync(filepath)) return
|
||||
const xml = new XML(parsedChannels)
|
||||
storage.saveSync(filepath, xml.toString())
|
||||
storage.saveSync(filepath, channelList.toString())
|
||||
logger.info(`\nFile '${filepath}' successfully saved`)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const xsd = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xs:attribute use="required" ref="site_id"/>
|
||||
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
|
||||
<xs:attribute name="logo" type="xs:string"/>
|
||||
<xs:attribute name="lcn" type="xs:string"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:attribute name="site">
|
||||
@@ -44,7 +45,7 @@ const xsd = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
</xs:attribute>
|
||||
</xs:schema>`
|
||||
|
||||
program.argument('[filepath]', 'Path to *.channels.xml files to check').parse(process.argv)
|
||||
program.argument('[filepath...]', 'Path to *.channels.xml files to check').parse(process.argv)
|
||||
|
||||
async function main() {
|
||||
const storage = new Storage()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Logger, File, Collection, Storage } from '@freearhey/core'
|
||||
import { ChannelsParser, XML } from '../../core'
|
||||
import { Channel } from 'epg-grabber'
|
||||
import { Command } from 'commander'
|
||||
import { Logger, File, Storage } from '@freearhey/core'
|
||||
import { ChannelsParser } from '../../core'
|
||||
import { ChannelList } from '../../models'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
import { Command } from 'commander'
|
||||
|
||||
const program = new Command()
|
||||
program
|
||||
@@ -11,7 +12,7 @@ program
|
||||
.option('-o, --output <output>', 'Output file')
|
||||
.parse(process.argv)
|
||||
|
||||
type ParseOptions = {
|
||||
interface ParseOptions {
|
||||
config: string
|
||||
set?: string
|
||||
output?: string
|
||||
@@ -21,22 +22,28 @@ type ParseOptions = {
|
||||
const options: ParseOptions = program.opts()
|
||||
|
||||
async function main() {
|
||||
function isPromise(promise: object[] | Promise<object[]>) {
|
||||
return (
|
||||
!!promise &&
|
||||
typeof promise === 'object' &&
|
||||
typeof (promise as Promise<object[]>).then === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
const storage = new Storage()
|
||||
const parser = new ChannelsParser({ storage })
|
||||
const logger = new Logger()
|
||||
const parser = new ChannelsParser({ storage })
|
||||
const file = new File(options.config)
|
||||
const dir = file.dirname()
|
||||
const config = (await import(pathToFileURL(options.config).toString())).default
|
||||
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
||||
|
||||
let channels = new Collection()
|
||||
let channelList = new ChannelList({ channels: [] })
|
||||
if (await storage.exists(outputFilepath)) {
|
||||
channels = await parser.parse(outputFilepath)
|
||||
channelList = await parser.parse(outputFilepath)
|
||||
}
|
||||
|
||||
const args: {
|
||||
[key: string]: string
|
||||
} = {}
|
||||
const args: Record<string, string> = {}
|
||||
|
||||
if (Array.isArray(options.set)) {
|
||||
options.set.forEach((arg: string) => {
|
||||
@@ -49,45 +56,31 @@ async function main() {
|
||||
if (isPromise(parsedChannels)) {
|
||||
parsedChannels = await parsedChannels
|
||||
}
|
||||
parsedChannels = parsedChannels.map((channel: Channel) => {
|
||||
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
|
||||
channel.site = config.site
|
||||
|
||||
return channel
|
||||
})
|
||||
|
||||
let output = new Collection()
|
||||
parsedChannels.forEach((channel: Channel) => {
|
||||
const found: Channel | undefined = channels.first(
|
||||
(_channel: Channel) => _channel.site_id == channel.site_id
|
||||
)
|
||||
const newChannelList = new ChannelList({ channels: [] })
|
||||
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
||||
if (!channel.site_id) return
|
||||
|
||||
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
|
||||
|
||||
if (found) {
|
||||
channel.xmltv_id = found.xmltv_id
|
||||
channel.lang = found.lang
|
||||
}
|
||||
|
||||
output.add(channel)
|
||||
newChannelList.add(channel)
|
||||
})
|
||||
|
||||
output = output.orderBy([
|
||||
(channel: Channel) => channel.lang || '_',
|
||||
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
||||
(channel: Channel) => channel.site_id
|
||||
])
|
||||
newChannelList.sort()
|
||||
|
||||
const xml = new XML(output)
|
||||
|
||||
await storage.save(outputFilepath, xml.toString())
|
||||
await storage.save(outputFilepath, newChannelList.toString())
|
||||
|
||||
logger.info(`File '${outputFilepath}' successfully saved`)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
function isPromise(promise: object[] | Promise<object[]>) {
|
||||
return (
|
||||
!!promise &&
|
||||
typeof promise === 'object' &&
|
||||
typeof (promise as Promise<object[]>).then === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Storage, Collection, Dictionary, File } from '@freearhey/core'
|
||||
import { ChannelsParser } from '../../core'
|
||||
import { Channel } from '../../models'
|
||||
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
|
||||
import { DataProcessorData } from '../../types/dataProcessor'
|
||||
import { Storage, Dictionary, File } from '@freearhey/core'
|
||||
import { DataLoaderData } from '../../types/dataLoader'
|
||||
import { ChannelList } from '../../models'
|
||||
import { DATA_DIR } from '../../constants'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
import { program } from 'commander'
|
||||
import chalk from 'chalk'
|
||||
import langs from 'langs'
|
||||
import { DATA_DIR } from '../../constants'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
|
||||
program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv)
|
||||
program.argument('[filepath...]', 'Path to *.channels.xml files to validate').parse(process.argv)
|
||||
|
||||
type ValidationError = {
|
||||
type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang'
|
||||
interface ValidationError {
|
||||
type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang'
|
||||
name: string
|
||||
lang?: string
|
||||
xmltv_id?: string
|
||||
@@ -19,15 +21,18 @@ type ValidationError = {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const parser = new ChannelsParser({ storage: new Storage() })
|
||||
|
||||
const processor = new DataProcessor()
|
||||
const dataStorage = new Storage(DATA_DIR)
|
||||
const channelsData = await dataStorage.json('channels.json')
|
||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
||||
const channelsGroupedById = channels.groupBy((channel: Channel) => channel.id)
|
||||
const loader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await loader.load()
|
||||
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
|
||||
const parser = new ChannelsParser({
|
||||
storage: new Storage()
|
||||
})
|
||||
|
||||
let totalFiles = 0
|
||||
let totalErrors = 0
|
||||
let totalWarnings = 0
|
||||
|
||||
const storage = new Storage()
|
||||
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
||||
@@ -35,11 +40,11 @@ async function main() {
|
||||
const file = new File(filepath)
|
||||
if (file.extension() !== 'xml') continue
|
||||
|
||||
const parsedChannels = await parser.parse(filepath)
|
||||
const channelList: ChannelList = await parser.parse(filepath)
|
||||
|
||||
const bufferBySiteId = new Dictionary()
|
||||
const errors: ValidationError[] = []
|
||||
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
||||
channelList.channels.forEach((channel: epgGrabber.Channel) => {
|
||||
const bufferId: string = channel.site_id
|
||||
if (bufferBySiteId.missing(bufferId)) {
|
||||
bufferBySiteId.set(bufferId, true)
|
||||
@@ -48,17 +53,26 @@ async function main() {
|
||||
totalErrors++
|
||||
}
|
||||
|
||||
if (!langs.where('1', channel.lang)) {
|
||||
if (!langs.where('1', channel.lang ?? '')) {
|
||||
errors.push({ type: 'wrong_lang', ...channel })
|
||||
totalErrors++
|
||||
}
|
||||
|
||||
if (!channel.xmltv_id) return
|
||||
const [channelId] = channel.xmltv_id.split('@')
|
||||
const foundChannel = channelsGroupedById.get(channelId)
|
||||
const [channelId, feedId] = channel.xmltv_id.split('@')
|
||||
|
||||
const foundChannel = channelsKeyById.get(channelId)
|
||||
if (!foundChannel) {
|
||||
errors.push({ type: 'wrong_xmltv_id', ...channel })
|
||||
totalErrors++
|
||||
errors.push({ type: 'wrong_channel_id', ...channel })
|
||||
totalWarnings++
|
||||
}
|
||||
|
||||
if (feedId) {
|
||||
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||
if (!foundFeed) {
|
||||
errors.push({ type: 'wrong_feed_id', ...channel })
|
||||
totalWarnings++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,10 +84,17 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const totalProblems = totalWarnings + totalErrors
|
||||
if (totalProblems > 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)`
|
||||
)
|
||||
)
|
||||
if (totalErrors > 0) {
|
||||
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
||||
import { Option, program } from 'commander'
|
||||
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
||||
import { Option, program } from 'commander'
|
||||
import { SITES_DIR } from '../../constants'
|
||||
import { Channel } from 'epg-grabber'
|
||||
import path from 'path'
|
||||
import { SITES_DIR } from '../../constants'
|
||||
import { ChannelList } from '../../models'
|
||||
|
||||
program
|
||||
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
|
||||
@@ -44,13 +45,15 @@ program
|
||||
.default(false)
|
||||
.env('GZIP')
|
||||
)
|
||||
.parse(process.argv)
|
||||
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
|
||||
.parse()
|
||||
|
||||
export type GrabOptions = {
|
||||
export interface GrabOptions {
|
||||
site?: string
|
||||
channels?: string
|
||||
output: string
|
||||
gzip: boolean
|
||||
curl: boolean
|
||||
maxConnections: number
|
||||
timeout?: string
|
||||
delay?: string
|
||||
@@ -85,31 +88,35 @@ async function main() {
|
||||
files = await storage.list(options.channels)
|
||||
}
|
||||
|
||||
let parsedChannels = new Collection()
|
||||
let channels = new Collection()
|
||||
for (const filepath of files) {
|
||||
parsedChannels = parsedChannels.concat(await parser.parse(filepath))
|
||||
const channelList: ChannelList = await parser.parse(filepath)
|
||||
|
||||
channels = channels.concat(channelList.channels)
|
||||
}
|
||||
|
||||
if (options.lang) {
|
||||
parsedChannels = parsedChannels.filter((channel: Channel) => {
|
||||
channels = channels.filter((channel: Channel) => {
|
||||
if (!options.lang || !channel.lang) return true
|
||||
|
||||
return options.lang.includes(channel.lang)
|
||||
})
|
||||
}
|
||||
logger.info(` found ${parsedChannels.count()} channel(s)`)
|
||||
|
||||
logger.info(` found ${channels.count()} channel(s)`)
|
||||
|
||||
logger.info('run:')
|
||||
runJob({ logger, parsedChannels })
|
||||
runJob({ logger, channels })
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function runJob({ logger, parsedChannels }: { logger: Logger; parsedChannels: Collection }) {
|
||||
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
|
||||
const timer = new Timer()
|
||||
timer.start()
|
||||
|
||||
const queueCreator = new QueueCreator({
|
||||
parsedChannels,
|
||||
channels,
|
||||
logger,
|
||||
options
|
||||
})
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { IssueLoader, HTMLTable, ChannelsParser } from '../../core'
|
||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
||||
import { ChannelList, Issue, Site } from '../../models'
|
||||
import { SITES_DIR, ROOT_DIR } from '../../constants'
|
||||
import { Issue, Site } from '../../models'
|
||||
import { Channel } from 'epg-grabber'
|
||||
|
||||
async function main() {
|
||||
const logger = new Logger({ disabled: true })
|
||||
const loader = new IssueLoader()
|
||||
const logger = new Logger({ level: -999 })
|
||||
const issueLoader = new IssueLoader()
|
||||
const sitesStorage = new Storage(SITES_DIR)
|
||||
const channelsParser = new ChannelsParser({ storage: sitesStorage })
|
||||
const sites = new Collection()
|
||||
|
||||
logger.info('loading channels...')
|
||||
const channelsParser = new ChannelsParser({
|
||||
storage: sitesStorage
|
||||
})
|
||||
|
||||
logger.info('loading list of sites')
|
||||
const folders = await sitesStorage.list('*/')
|
||||
|
||||
logger.info('loading issues...')
|
||||
const issues = await loader.load()
|
||||
const issues = await issueLoader.load()
|
||||
|
||||
logger.info('putting the data together...')
|
||||
const brokenGuideReports = issues.filter(issue =>
|
||||
@@ -33,19 +37,21 @@ async function main() {
|
||||
|
||||
const files = await sitesStorage.list(`${domain}/*.channels.xml`)
|
||||
for (const filepath of files) {
|
||||
const channels = await channelsParser.parse(filepath)
|
||||
const channelList: ChannelList = await channelsParser.parse(filepath)
|
||||
|
||||
site.totalChannels += channels.count()
|
||||
site.markedChannels += channels.filter((channel: Channel) => channel.xmltv_id).count()
|
||||
site.totalChannels += channelList.channels.count()
|
||||
site.markedChannels += channelList.channels
|
||||
.filter((channel: Channel) => channel.xmltv_id)
|
||||
.count()
|
||||
}
|
||||
|
||||
sites.add(site)
|
||||
}
|
||||
|
||||
logger.info('creating sites table...')
|
||||
const data = new Collection()
|
||||
const tableData = new Collection()
|
||||
sites.forEach((site: Site) => {
|
||||
data.add([
|
||||
tableData.add([
|
||||
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` },
|
||||
{ value: site.totalChannels, align: 'right' },
|
||||
{ value: site.markedChannels, align: 'right' },
|
||||
@@ -55,7 +61,7 @@ async function main() {
|
||||
})
|
||||
|
||||
logger.info('updating sites.md...')
|
||||
const table = new HTMLTable(data.all(), [
|
||||
const table = new HTMLTable(tableData.all(), [
|
||||
{ name: 'Site', align: 'left' },
|
||||
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
|
||||
{ name: 'Status', align: 'left' },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { parseChannels } from 'epg-grabber'
|
||||
import { Storage, Collection } from '@freearhey/core'
|
||||
import { Storage } from '@freearhey/core'
|
||||
import { ChannelList } from '../models'
|
||||
|
||||
type ChannelsParserProps = {
|
||||
interface ChannelsParserProps {
|
||||
storage: Storage
|
||||
}
|
||||
|
||||
@@ -12,13 +13,10 @@ export class ChannelsParser {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
async parse(filepath: string) {
|
||||
let parsedChannels = new Collection()
|
||||
|
||||
async parse(filepath: string): Promise<ChannelList> {
|
||||
const content = await this.storage.load(filepath)
|
||||
const channels = parseChannels(content)
|
||||
parsedChannels = parsedChannels.concat(new Collection(channels))
|
||||
const parsed = parseChannels(content)
|
||||
|
||||
return parsedChannels
|
||||
return new ChannelList({ channels: parsed })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SiteConfig } from 'epg-grabber'
|
||||
import _ from 'lodash'
|
||||
import { pathToFileURL } from 'url'
|
||||
|
||||
export class ConfigLoader {
|
||||
@@ -28,6 +27,6 @@ export class ConfigLoader {
|
||||
channels: undefined
|
||||
}
|
||||
|
||||
return _.merge(defaultConfig, config)
|
||||
return { ...defaultConfig, ...config } as SiteConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ export class DataLoader {
|
||||
feeds,
|
||||
timezones,
|
||||
guides,
|
||||
streams
|
||||
streams,
|
||||
logos
|
||||
] = await Promise.all([
|
||||
this.storage.json('countries.json'),
|
||||
this.storage.json('regions.json'),
|
||||
@@ -61,7 +62,8 @@ export class DataLoader {
|
||||
this.storage.json('feeds.json'),
|
||||
this.storage.json('timezones.json'),
|
||||
this.storage.json('guides.json'),
|
||||
this.storage.json('streams.json')
|
||||
this.storage.json('streams.json'),
|
||||
this.storage.json('logos.json')
|
||||
])
|
||||
|
||||
return {
|
||||
@@ -75,7 +77,8 @@ export class DataLoader {
|
||||
feeds,
|
||||
timezones,
|
||||
guides,
|
||||
streams
|
||||
streams,
|
||||
logos
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,55 @@
|
||||
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
|
||||
import { DataLoaderData } from '../types/dataLoader'
|
||||
import { Collection } from '@freearhey/core'
|
||||
import { Channel, Feed, Guide, Stream } from '../models'
|
||||
|
||||
export class DataProcessor {
|
||||
constructor() {}
|
||||
|
||||
process(data: DataLoaderData) {
|
||||
let channels = new Collection(data.channels).map(data => new Channel(data))
|
||||
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||
|
||||
const guides = new Collection(data.guides).map(data => new Guide(data))
|
||||
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId())
|
||||
const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
|
||||
const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
|
||||
channel.getStreamId()
|
||||
)
|
||||
|
||||
const streams = new Collection(data.streams).map(data => new Stream(data))
|
||||
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||
|
||||
const feeds = new Collection(data.feeds).map(data =>
|
||||
let feeds = new Collection(data.feeds).map(data =>
|
||||
new Feed(data)
|
||||
.withGuides(guidesGroupedByStreamId)
|
||||
.withGuideChannels(guideChannelsGroupedByStreamId)
|
||||
.withStreams(streamsGroupedById)
|
||||
.withChannel(channelsKeyById)
|
||||
)
|
||||
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
|
||||
|
||||
const logos = new Collection(data.logos).map(data =>
|
||||
new Logo(data).withFeed(feedsKeyByStreamId)
|
||||
)
|
||||
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
||||
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
||||
|
||||
feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId))
|
||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||
|
||||
channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId))
|
||||
channels = channels.map((channel: Channel) =>
|
||||
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
||||
)
|
||||
|
||||
return {
|
||||
guideChannelsGroupedByStreamId,
|
||||
feedsGroupedByChannelId,
|
||||
guidesGroupedByStreamId,
|
||||
logosGroupedByChannelId,
|
||||
logosGroupedByStreamId,
|
||||
streamsGroupedById,
|
||||
feedsKeyByStreamId,
|
||||
channelsKeyById,
|
||||
guideChannels,
|
||||
channels,
|
||||
streams,
|
||||
guides,
|
||||
feeds
|
||||
feeds,
|
||||
logos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GrabOptions } from '../commands/epg/grab'
|
||||
import { TaskQueue, PromisyClass } from 'cwait'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
|
||||
type GrabberProps = {
|
||||
interface GrabberProps {
|
||||
logger: Logger
|
||||
queue: Queue
|
||||
options: GrabOptions
|
||||
@@ -70,6 +70,10 @@ export class Grabber {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.curl === true) {
|
||||
config.curl = true
|
||||
}
|
||||
|
||||
const _programs = await this.grabber.grab(
|
||||
channel,
|
||||
date,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core'
|
||||
import { Channel } from 'epg-grabber'
|
||||
import { XMLTV } from '../core'
|
||||
import path from 'path'
|
||||
|
||||
type GuideProps = {
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
logger: Logger
|
||||
filepath: string
|
||||
gzip: boolean
|
||||
}
|
||||
|
||||
export class Guide {
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
logger: Logger
|
||||
storage: Storage
|
||||
filepath: string
|
||||
gzip: boolean
|
||||
|
||||
constructor({ channels, programs, logger, filepath, gzip }: GuideProps) {
|
||||
this.channels = channels
|
||||
this.programs = programs
|
||||
this.logger = logger
|
||||
this.storage = new Storage(path.dirname(filepath))
|
||||
this.filepath = filepath
|
||||
this.gzip = gzip || false
|
||||
}
|
||||
|
||||
async save() {
|
||||
const channels = this.channels.uniqBy(
|
||||
(channel: Channel) => `${channel.xmltv_id}:${channel.site}`
|
||||
)
|
||||
const programs = this.programs
|
||||
|
||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
||||
zone: 'UTC'
|
||||
})
|
||||
const xmltv = new XMLTV({
|
||||
channels,
|
||||
programs,
|
||||
date: currDate
|
||||
})
|
||||
|
||||
const xmlFilepath = this.filepath
|
||||
const xmlFilename = path.basename(xmlFilepath)
|
||||
this.logger.info(` saving to "${xmlFilepath}"...`)
|
||||
await this.storage.save(xmlFilename, xmltv.toString())
|
||||
|
||||
if (this.gzip) {
|
||||
const zip = new Zip()
|
||||
const compressed = await zip.compress(xmltv.toString())
|
||||
const gzFilepath = `${this.filepath}.gz`
|
||||
const gzFilename = path.basename(gzFilepath)
|
||||
this.logger.info(` saving to "${gzFilepath}"...`)
|
||||
await this.storage.save(gzFilename, compressed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core'
|
||||
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
import { OptionValues } from 'commander'
|
||||
import { Channel, Program } from 'epg-grabber'
|
||||
import { Guide } from '.'
|
||||
import { Channel, Feed, Guide } from '../models'
|
||||
import path from 'path'
|
||||
import { DataLoader, DataProcessor } from '.'
|
||||
import { DataLoaderData } from '../types/dataLoader'
|
||||
import { DataProcessorData } from '../types/dataProcessor'
|
||||
import { DATA_DIR } from '../constants'
|
||||
|
||||
type GuideManagerProps = {
|
||||
interface GuideManagerProps {
|
||||
options: OptionValues
|
||||
logger: Logger
|
||||
channels: Collection
|
||||
@@ -12,7 +17,6 @@ type GuideManagerProps = {
|
||||
|
||||
export class GuideManager {
|
||||
options: OptionValues
|
||||
storage: Storage
|
||||
logger: Logger
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
@@ -22,22 +26,51 @@ export class GuideManager {
|
||||
this.logger = logger
|
||||
this.channels = channels
|
||||
this.programs = programs
|
||||
this.storage = new Storage()
|
||||
}
|
||||
|
||||
async createGuides() {
|
||||
const pathTemplate = new StringTemplate(this.options.output)
|
||||
|
||||
const processor = new DataProcessor()
|
||||
const dataStorage = new Storage(DATA_DIR)
|
||||
const loader = new DataLoader({ storage: dataStorage })
|
||||
const data: DataLoaderData = await loader.load()
|
||||
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
|
||||
|
||||
const groupedChannels = this.channels
|
||||
.orderBy([(channel: Channel) => channel.xmltv_id])
|
||||
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`)
|
||||
.groupBy((channel: Channel) => {
|
||||
.map((channel: epgGrabber.Channel) => {
|
||||
if (channel.xmltv_id && !channel.icon) {
|
||||
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||
if (foundFeed && foundFeed.hasLogo()) {
|
||||
channel.icon = foundFeed.getLogoUrl()
|
||||
} else {
|
||||
const [channelId] = channel.xmltv_id.split('@')
|
||||
const foundChannel: Channel = channelsKeyById.get(channelId)
|
||||
if (foundChannel && foundChannel.hasLogo()) {
|
||||
channel.icon = foundChannel.getLogoUrl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channel
|
||||
})
|
||||
.orderBy([
|
||||
(channel: epgGrabber.Channel) => channel.index,
|
||||
(channel: epgGrabber.Channel) => channel.xmltv_id
|
||||
])
|
||||
.uniqBy(
|
||||
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
|
||||
)
|
||||
.groupBy((channel: epgGrabber.Channel) => {
|
||||
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
|
||||
})
|
||||
|
||||
const groupedPrograms = this.programs
|
||||
.orderBy([(program: Program) => program.channel, (program: Program) => program.start])
|
||||
.groupBy((program: Program) => {
|
||||
.orderBy([
|
||||
(program: epgGrabber.Program) => program.channel,
|
||||
(program: epgGrabber.Program) => program.start
|
||||
])
|
||||
.groupBy((program: epgGrabber.Program) => {
|
||||
const lang =
|
||||
program.titles && program.titles.length && program.titles[0].lang
|
||||
? program.titles[0].lang
|
||||
@@ -51,11 +84,28 @@ export class GuideManager {
|
||||
filepath: groupKey,
|
||||
gzip: this.options.gzip,
|
||||
channels: new Collection(groupedChannels.get(groupKey)),
|
||||
programs: new Collection(groupedPrograms.get(groupKey)),
|
||||
logger: this.logger
|
||||
programs: new Collection(groupedPrograms.get(groupKey))
|
||||
})
|
||||
|
||||
await guide.save()
|
||||
await this.save(guide)
|
||||
}
|
||||
}
|
||||
|
||||
async save(guide: Guide) {
|
||||
const storage = new Storage(path.dirname(guide.filepath))
|
||||
const xmlFilepath = guide.filepath
|
||||
const xmlFilename = path.basename(xmlFilepath)
|
||||
this.logger.info(` saving to "${xmlFilepath}"...`)
|
||||
const xmltv = guide.toString()
|
||||
await storage.save(xmlFilename, xmltv)
|
||||
|
||||
if (guide.gzip) {
|
||||
const zip = new Zip()
|
||||
const compressed = zip.compress(xmltv)
|
||||
const gzFilepath = `${guide.filepath}.gz`
|
||||
const gzFilename = path.basename(gzFilepath)
|
||||
this.logger.info(` saving to "${gzFilepath}"...`)
|
||||
await storage.save(gzFilename, compressed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type Column = {
|
||||
interface Column {
|
||||
name: string
|
||||
nowrap?: boolean
|
||||
align?: string
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from './configLoader'
|
||||
export * from './dataLoader'
|
||||
export * from './dataProcessor'
|
||||
export * from './grabber'
|
||||
export * from './guide'
|
||||
export * from './guideManager'
|
||||
export * from './htmlTable'
|
||||
export * from './issueLoader'
|
||||
@@ -13,5 +12,3 @@ export * from './job'
|
||||
export * from './proxyParser'
|
||||
export * from './queue'
|
||||
export * from './queueCreator'
|
||||
export * from './xml'
|
||||
export * from './xmltv'
|
||||
|
||||
@@ -23,6 +23,7 @@ export class IssueLoader {
|
||||
repo: REPO,
|
||||
per_page: 100,
|
||||
labels,
|
||||
state: 'open',
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Logger } from '@freearhey/core'
|
||||
import { Queue, Grabber, GuideManager } from '.'
|
||||
import { GrabOptions } from '../commands/epg/grab'
|
||||
|
||||
type JobProps = {
|
||||
interface JobProps {
|
||||
options: GrabOptions
|
||||
logger: Logger
|
||||
queue: Queue
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { URL } from 'node:url'
|
||||
|
||||
type ProxyParserResult = {
|
||||
interface ProxyParserResult {
|
||||
protocol: string | null
|
||||
auth: {
|
||||
username: string | null
|
||||
password: string | null
|
||||
auth?: {
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
host: string
|
||||
port: number | null
|
||||
@@ -14,14 +14,18 @@ export class ProxyParser {
|
||||
parse(_url: string): ProxyParserResult {
|
||||
const parsed = new URL(_url)
|
||||
|
||||
return {
|
||||
const result: ProxyParserResult = {
|
||||
protocol: parsed.protocol.replace(':', '') || null,
|
||||
auth: {
|
||||
username: parsed.username || null,
|
||||
password: parsed.password || null
|
||||
},
|
||||
host: parsed.hostname,
|
||||
port: parsed.port ? parseInt(parsed.port) : null
|
||||
}
|
||||
|
||||
if (parsed.username || parsed.password) {
|
||||
result.auth = {}
|
||||
if (parsed.username) result.auth.username = parsed.username
|
||||
if (parsed.password) result.auth.password = parsed.password
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
import { SiteConfig, Channel } from 'epg-grabber'
|
||||
|
||||
export type QueueItem = {
|
||||
export interface QueueItem {
|
||||
channel: Channel
|
||||
date: string
|
||||
config: SiteConfig
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
||||
import { ChannelsParser, ConfigLoader, Queue } from './'
|
||||
import { SITES_DIR, DATA_DIR } from '../constants'
|
||||
import { GrabOptions } from '../commands/epg/grab'
|
||||
import { ConfigLoader, Queue } from './'
|
||||
import { SiteConfig } from 'epg-grabber'
|
||||
import path from 'path'
|
||||
import { GrabOptions } from '../commands/epg/grab'
|
||||
import { Channel } from '../models'
|
||||
|
||||
type QueueCreatorProps = {
|
||||
interface QueueCreatorProps {
|
||||
logger: Logger
|
||||
options: GrabOptions
|
||||
parsedChannels: Collection
|
||||
channels: Collection
|
||||
}
|
||||
|
||||
export class QueueCreator {
|
||||
@@ -17,42 +16,29 @@ export class QueueCreator {
|
||||
logger: Logger
|
||||
sitesStorage: Storage
|
||||
dataStorage: Storage
|
||||
parser: ChannelsParser
|
||||
parsedChannels: Collection
|
||||
channels: Collection
|
||||
options: GrabOptions
|
||||
|
||||
constructor({ parsedChannels, logger, options }: QueueCreatorProps) {
|
||||
this.parsedChannels = parsedChannels
|
||||
constructor({ channels, logger, options }: QueueCreatorProps) {
|
||||
this.channels = channels
|
||||
this.logger = logger
|
||||
this.sitesStorage = new Storage()
|
||||
this.dataStorage = new Storage(DATA_DIR)
|
||||
this.parser = new ChannelsParser({ storage: new Storage() })
|
||||
this.options = options
|
||||
this.configLoader = new ConfigLoader()
|
||||
}
|
||||
|
||||
async create(): Promise<Queue> {
|
||||
const channelsContent = await this.dataStorage.json('channels.json')
|
||||
const channels = new Collection(channelsContent).map(data => new Channel(data))
|
||||
|
||||
let index = 0
|
||||
const queue = new Queue()
|
||||
for (const channel of this.parsedChannels.all()) {
|
||||
for (const channel of this.channels.all()) {
|
||||
channel.index = index++
|
||||
if (!channel.site || !channel.site_id || !channel.name) continue
|
||||
|
||||
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
||||
const config: SiteConfig = await this.configLoader.load(configPath)
|
||||
|
||||
if (channel.xmltv_id) {
|
||||
if (!channel.icon) {
|
||||
const found: Channel = channels.first(
|
||||
(_channel: Channel) => _channel.id === channel.xmltv_id
|
||||
)
|
||||
|
||||
if (found) {
|
||||
channel.icon = found.logo
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!channel.xmltv_id) {
|
||||
channel.xmltv_id = channel.site_id
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Collection } from '@freearhey/core'
|
||||
import { Channel } from 'epg-grabber'
|
||||
|
||||
export class XML {
|
||||
items: Collection
|
||||
|
||||
constructor(items: Collection) {
|
||||
this.items = items
|
||||
}
|
||||
|
||||
toString() {
|
||||
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
|
||||
|
||||
this.items.forEach((channel: Channel) => {
|
||||
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
|
||||
const xmltv_id = channel.xmltv_id || ''
|
||||
const lang = channel.lang || ''
|
||||
const site_id = channel.site_id || ''
|
||||
output += ` <channel site="${channel.site}" lang="${lang}" xmltv_id="${escapeString(
|
||||
xmltv_id
|
||||
)}" site_id="${site_id}"${logo}>${escapeString(channel.name)}</channel>\r\n`
|
||||
})
|
||||
|
||||
output += '</channels>\r\n'
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
function escapeString(value: string, defaultValue: string = '') {
|
||||
if (!value) return defaultValue
|
||||
|
||||
const regex = new RegExp(
|
||||
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
||||
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
||||
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
||||
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
||||
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
||||
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
||||
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
||||
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
||||
'g'
|
||||
)
|
||||
|
||||
value = String(value || '').replace(regex, '')
|
||||
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n|\r/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { DateTime, Collection } from '@freearhey/core'
|
||||
import { generateXMLTV } from 'epg-grabber'
|
||||
|
||||
type XMLTVProps = {
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
date: DateTime
|
||||
}
|
||||
|
||||
export class XMLTV {
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
date: DateTime
|
||||
|
||||
constructor({ channels, programs, date }: XMLTVProps) {
|
||||
this.channels = channels
|
||||
this.programs = programs
|
||||
this.date = date
|
||||
}
|
||||
|
||||
toString() {
|
||||
return generateXMLTV({
|
||||
channels: this.channels.all(),
|
||||
programs: this.programs.all(),
|
||||
date: this.date.toJSON()
|
||||
})
|
||||
}
|
||||
}
|
||||
+68
-17
@@ -1,26 +1,28 @@
|
||||
import { ChannelData, ChannelSearchableData } from '../types/channel'
|
||||
import { Collection, Dictionary } from '@freearhey/core'
|
||||
import { Stream, Guide, Feed } from './'
|
||||
import { Stream, Feed, Logo, GuideChannel } from './'
|
||||
|
||||
export class Channel {
|
||||
id: string
|
||||
name: string
|
||||
id?: string
|
||||
name?: string
|
||||
altNames?: Collection
|
||||
network?: string
|
||||
owners?: Collection
|
||||
countryCode: string
|
||||
countryCode?: string
|
||||
subdivisionCode?: string
|
||||
cityName?: string
|
||||
categoryIds?: Collection
|
||||
isNSFW: boolean
|
||||
isNSFW = false
|
||||
launched?: string
|
||||
closed?: string
|
||||
replacedBy?: string
|
||||
website?: string
|
||||
logo?: string
|
||||
feeds?: Collection
|
||||
logos: Collection = new Collection()
|
||||
|
||||
constructor(data?: ChannelData) {
|
||||
if (!data) return
|
||||
|
||||
constructor(data: ChannelData) {
|
||||
this.id = data.id
|
||||
this.name = data.name
|
||||
this.altNames = new Collection(data.alt_names)
|
||||
@@ -35,11 +37,16 @@ export class Channel {
|
||||
this.closed = data.closed || undefined
|
||||
this.replacedBy = data.replaced_by || undefined
|
||||
this.website = data.website || undefined
|
||||
this.logo = data.logo
|
||||
}
|
||||
|
||||
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
||||
this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
||||
if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
withLogos(logosGroupedByChannelId: Dictionary): this {
|
||||
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -50,19 +57,19 @@ export class Channel {
|
||||
return this.feeds
|
||||
}
|
||||
|
||||
getGuides(): Collection {
|
||||
let guides = new Collection()
|
||||
getGuideChannels(): Collection {
|
||||
let channels = new Collection()
|
||||
|
||||
this.getFeeds().forEach((feed: Feed) => {
|
||||
guides = guides.concat(feed.getGuides())
|
||||
channels = channels.concat(feed.getGuideChannels())
|
||||
})
|
||||
|
||||
return guides
|
||||
return channels
|
||||
}
|
||||
|
||||
getGuideNames(): Collection {
|
||||
return this.getGuides()
|
||||
.map((guide: Guide) => guide.siteName)
|
||||
getGuideChannelNames(): Collection {
|
||||
return this.getGuideChannels()
|
||||
.map((channel: GuideChannel) => channel.siteName)
|
||||
.uniq()
|
||||
}
|
||||
|
||||
@@ -100,12 +107,56 @@ export class Channel {
|
||||
return this.altNames || new Collection()
|
||||
}
|
||||
|
||||
getLogos(): Collection {
|
||||
function feed(logo: Logo): number {
|
||||
if (!logo.feed) return 1
|
||||
if (logo.feed.isMain) return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function format(logo: Logo): number {
|
||||
const levelByFormat: Record<string, number> = {
|
||||
SVG: 0,
|
||||
PNG: 3,
|
||||
APNG: 1,
|
||||
WebP: 1,
|
||||
AVIF: 1,
|
||||
JPEG: 2,
|
||||
GIF: 1
|
||||
}
|
||||
|
||||
return logo.format ? levelByFormat[logo.format] : 0
|
||||
}
|
||||
|
||||
function size(logo: Logo): number {
|
||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||
}
|
||||
|
||||
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
|
||||
}
|
||||
|
||||
getLogo(): Logo | undefined {
|
||||
return this.getLogos().first()
|
||||
}
|
||||
|
||||
hasLogo(): boolean {
|
||||
return this.getLogos().notEmpty()
|
||||
}
|
||||
|
||||
getLogoUrl(): string {
|
||||
const logo = this.getLogo()
|
||||
if (!logo) return ''
|
||||
|
||||
return logo.url || ''
|
||||
}
|
||||
|
||||
getSearchable(): ChannelSearchableData {
|
||||
return {
|
||||
id: this.getId(),
|
||||
name: this.getName(),
|
||||
altNames: this.getAltNames().all(),
|
||||
guideNames: this.getGuideNames().all(),
|
||||
guideNames: this.getGuideChannelNames().all(),
|
||||
streamNames: this.getStreamNames().all(),
|
||||
feedFullNames: this.getFeedFullNames().all()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Collection } from '@freearhey/core'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
|
||||
export class ChannelList {
|
||||
channels: Collection = new Collection()
|
||||
|
||||
constructor(data: { channels: epgGrabber.Channel[] }) {
|
||||
this.channels = new Collection(data.channels)
|
||||
}
|
||||
|
||||
add(channel: epgGrabber.Channel): this {
|
||||
this.channels.add(channel)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
get(siteId: string): epgGrabber.Channel | undefined {
|
||||
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
|
||||
}
|
||||
|
||||
sort(): this {
|
||||
this.channels = this.channels.orderBy([
|
||||
(channel: epgGrabber.Channel) => channel.lang || '_',
|
||||
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
||||
(channel: epgGrabber.Channel) => channel.site_id
|
||||
])
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
toString() {
|
||||
function escapeString(value: string, defaultValue = '') {
|
||||
if (!value) return defaultValue
|
||||
|
||||
const regex = new RegExp(
|
||||
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
||||
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
||||
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
||||
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
||||
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
||||
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
||||
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
||||
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
||||
'g'
|
||||
)
|
||||
|
||||
value = String(value || '').replace(regex, '')
|
||||
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n|\r/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
|
||||
|
||||
this.channels.forEach((channel: epgGrabber.Channel) => {
|
||||
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
|
||||
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
|
||||
const lang = channel.lang || ''
|
||||
const site_id = channel.site_id || ''
|
||||
const site = channel.site || ''
|
||||
const displayName = channel.name ? escapeString(channel.name) : ''
|
||||
|
||||
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
|
||||
})
|
||||
|
||||
output += '</channels>\r\n'
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
+60
-8
@@ -1,6 +1,6 @@
|
||||
import { Collection, Dictionary } from '@freearhey/core'
|
||||
import { FeedData } from '../types/feed'
|
||||
import { Channel } from './channel'
|
||||
import { Logo, Channel } from '.'
|
||||
|
||||
export class Feed {
|
||||
channelId: string
|
||||
@@ -12,8 +12,9 @@ export class Feed {
|
||||
languageCodes: Collection
|
||||
timezoneIds: Collection
|
||||
videoFormat: string
|
||||
guides?: Collection
|
||||
guideChannels?: Collection
|
||||
streams?: Collection
|
||||
logos: Collection = new Collection()
|
||||
|
||||
constructor(data: FeedData) {
|
||||
this.channelId = data.channel
|
||||
@@ -42,20 +43,30 @@ export class Feed {
|
||||
return this
|
||||
}
|
||||
|
||||
withGuides(guidesGroupedByStreamId: Dictionary): this {
|
||||
this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`))
|
||||
withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
|
||||
this.guideChannels = new Collection(
|
||||
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
|
||||
)
|
||||
|
||||
if (this.isMain) {
|
||||
this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId)))
|
||||
this.guideChannels = this.guideChannels.concat(
|
||||
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
|
||||
)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
getGuides(): Collection {
|
||||
if (!this.guides) return new Collection()
|
||||
withLogos(logosGroupedByStreamId: Dictionary): this {
|
||||
this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
|
||||
|
||||
return this.guides
|
||||
return this
|
||||
}
|
||||
|
||||
getGuideChannels(): Collection {
|
||||
if (!this.guideChannels) return new Collection()
|
||||
|
||||
return this.guideChannels
|
||||
}
|
||||
|
||||
getStreams(): Collection {
|
||||
@@ -69,4 +80,45 @@ export class Feed {
|
||||
|
||||
return `${this.channel.name} ${this.name}`
|
||||
}
|
||||
|
||||
getStreamId(): string {
|
||||
return `${this.channelId}@${this.id}`
|
||||
}
|
||||
|
||||
getLogos(): Collection {
|
||||
function format(logo: Logo): number {
|
||||
const levelByFormat: Record<string, number> = {
|
||||
SVG: 0,
|
||||
PNG: 3,
|
||||
APNG: 1,
|
||||
WebP: 1,
|
||||
AVIF: 1,
|
||||
JPEG: 2,
|
||||
GIF: 1
|
||||
}
|
||||
|
||||
return logo.format ? levelByFormat[logo.format] : 0
|
||||
}
|
||||
|
||||
function size(logo: Logo): number {
|
||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||
}
|
||||
|
||||
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
|
||||
}
|
||||
|
||||
getLogo(): Logo | undefined {
|
||||
return this.getLogos().first()
|
||||
}
|
||||
|
||||
hasLogo(): boolean {
|
||||
return this.getLogos().notEmpty()
|
||||
}
|
||||
|
||||
getLogoUrl(): string {
|
||||
const logo = this.getLogo()
|
||||
if (!logo) return ''
|
||||
|
||||
return logo.url || ''
|
||||
}
|
||||
}
|
||||
|
||||
+27
-27
@@ -1,35 +1,35 @@
|
||||
import type { GuideData } from '../types/guide'
|
||||
import { uniqueId } from 'lodash'
|
||||
import { Collection, DateTime } from '@freearhey/core'
|
||||
import { generateXMLTV } from 'epg-grabber'
|
||||
|
||||
interface GuideData {
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
filepath: string
|
||||
gzip: boolean
|
||||
}
|
||||
|
||||
export class Guide {
|
||||
channelId?: string
|
||||
feedId?: string
|
||||
siteDomain?: string
|
||||
siteId?: string
|
||||
siteName?: string
|
||||
languageCode?: string
|
||||
channels: Collection
|
||||
programs: Collection
|
||||
filepath: string
|
||||
gzip: boolean
|
||||
|
||||
constructor(data?: GuideData) {
|
||||
if (!data) return
|
||||
|
||||
this.channelId = data.channel
|
||||
this.feedId = data.feed
|
||||
this.siteDomain = data.site
|
||||
this.siteId = data.site_id
|
||||
this.siteName = data.site_name
|
||||
this.languageCode = data.lang
|
||||
constructor({ channels, programs, filepath, gzip }: GuideData) {
|
||||
this.channels = channels
|
||||
this.programs = programs
|
||||
this.filepath = filepath
|
||||
this.gzip = gzip || false
|
||||
}
|
||||
|
||||
getUUID(): string {
|
||||
if (!this.getStreamId() || !this.siteId) return uniqueId()
|
||||
toString() {
|
||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
||||
timezone: 'UTC'
|
||||
})
|
||||
|
||||
return this.getStreamId() + this.siteId
|
||||
}
|
||||
|
||||
getStreamId(): string | undefined {
|
||||
if (!this.channelId) return undefined
|
||||
if (!this.feedId) return this.channelId
|
||||
|
||||
return `${this.channelId}@${this.feedId}`
|
||||
return generateXMLTV({
|
||||
channels: this.channels.all(),
|
||||
programs: this.programs.all(),
|
||||
date: currDate.toJSON()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
import epgGrabber from 'epg-grabber'
|
||||
import { Feed, Channel } from '.'
|
||||
|
||||
export class GuideChannel {
|
||||
channelId?: string
|
||||
channel?: Channel
|
||||
feedId?: string
|
||||
feed?: Feed
|
||||
xmltvId?: string
|
||||
languageCode?: string
|
||||
siteId?: string
|
||||
logoUrl?: string
|
||||
siteDomain?: string
|
||||
siteName?: string
|
||||
|
||||
constructor(data: epgGrabber.Channel) {
|
||||
const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined]
|
||||
|
||||
this.channelId = channelId
|
||||
this.feedId = feedId
|
||||
this.xmltvId = data.xmltv_id
|
||||
this.languageCode = data.lang
|
||||
this.siteId = data.site_id
|
||||
this.logoUrl = data.logo
|
||||
this.siteDomain = data.site
|
||||
this.siteName = data.name
|
||||
}
|
||||
|
||||
withChannel(channelsKeyById: Dictionary): this {
|
||||
if (this.channelId) this.channel = channelsKeyById.get(this.channelId)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
withFeed(feedsKeyByStreamId: Dictionary): this {
|
||||
if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
getStreamId(): string {
|
||||
if (!this.channelId) return ''
|
||||
if (!this.feedId) return this.channelId
|
||||
|
||||
return `${this.channelId}@${this.feedId}`
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
channel: this.channelId || null,
|
||||
feed: this.feedId || null,
|
||||
site: this.siteDomain || '',
|
||||
site_id: this.siteId || '',
|
||||
site_name: this.siteName || '',
|
||||
lang: this.languageCode || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
export * from './issue'
|
||||
export * from './site'
|
||||
export * from './channel'
|
||||
export * from './feed'
|
||||
export * from './stream'
|
||||
export * from './guide'
|
||||
export * from './guideChannel'
|
||||
export * from './issue'
|
||||
export * from './logo'
|
||||
export * from './site'
|
||||
export * from './stream'
|
||||
export * from './channelList'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dictionary } from '@freearhey/core'
|
||||
import { OWNER, REPO } from '../constants'
|
||||
|
||||
type IssueProps = {
|
||||
interface IssueProps {
|
||||
number: number
|
||||
labels: string[]
|
||||
data: Dictionary
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Collection, type Dictionary } from '@freearhey/core'
|
||||
import type { LogoData } from '../types/logo'
|
||||
import { type Feed } from './feed'
|
||||
|
||||
export class Logo {
|
||||
channelId?: string
|
||||
feedId?: string
|
||||
feed?: Feed
|
||||
tags: Collection = new Collection()
|
||||
width = 0
|
||||
height = 0
|
||||
format?: string
|
||||
url?: string
|
||||
|
||||
constructor(data?: LogoData) {
|
||||
if (!data) return
|
||||
|
||||
this.channelId = data.channel
|
||||
this.feedId = data.feed || undefined
|
||||
this.tags = new Collection(data.tags)
|
||||
this.width = data.width
|
||||
this.height = data.height
|
||||
this.format = data.format || undefined
|
||||
this.url = data.url
|
||||
}
|
||||
|
||||
withFeed(feedsKeyByStreamId: Dictionary): this {
|
||||
if (!this.feedId) return this
|
||||
|
||||
this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
getStreamId(): string {
|
||||
if (!this.channelId) return ''
|
||||
if (!this.feedId) return this.channelId
|
||||
|
||||
return `${this.channelId}@${this.feedId}`
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ enum StatusCode {
|
||||
OK = 'ok'
|
||||
}
|
||||
|
||||
type Status = {
|
||||
interface Status {
|
||||
code: StatusCode
|
||||
emoji: string
|
||||
}
|
||||
|
||||
type SiteProps = {
|
||||
interface SiteProps {
|
||||
domain: string
|
||||
totalChannels?: number
|
||||
markedChannels?: number
|
||||
|
||||
@@ -16,8 +16,8 @@ export class Stream {
|
||||
isInterlaced?: boolean
|
||||
referrer?: string
|
||||
userAgent?: string
|
||||
groupTitle: string = 'Undefined'
|
||||
removed: boolean = false
|
||||
groupTitle = 'Undefined'
|
||||
removed = false
|
||||
|
||||
constructor(data: StreamData) {
|
||||
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
|
||||
|
||||
Vendored
+2
-3
@@ -1,6 +1,6 @@
|
||||
import { Collection } from '@freearhey/core'
|
||||
|
||||
export type ChannelData = {
|
||||
export interface ChannelData {
|
||||
id: string
|
||||
name: string
|
||||
alt_names: string[]
|
||||
@@ -15,10 +15,9 @@ export type ChannelData = {
|
||||
closed: string
|
||||
replaced_by: string
|
||||
website: string
|
||||
logo: string
|
||||
}
|
||||
|
||||
export type ChannelSearchableData = {
|
||||
export interface ChannelSearchableData {
|
||||
id: string
|
||||
name: string
|
||||
altNames: string[]
|
||||
|
||||
Vendored
+3
-2
@@ -1,10 +1,10 @@
|
||||
import { Storage } from '@freearhey/core'
|
||||
|
||||
export type DataLoaderProps = {
|
||||
export interface DataLoaderProps {
|
||||
storage: Storage
|
||||
}
|
||||
|
||||
export type DataLoaderData = {
|
||||
export interface DataLoaderData {
|
||||
countries: object | object[]
|
||||
regions: object | object[]
|
||||
subdivisions: object | object[]
|
||||
@@ -16,4 +16,5 @@ export type DataLoaderData = {
|
||||
timezones: object | object[]
|
||||
guides: object | object[]
|
||||
streams: object | object[]
|
||||
logos: object | object[]
|
||||
}
|
||||
|
||||
Vendored
+7
-3
@@ -1,12 +1,16 @@
|
||||
import { Collection, Dictionary } from '@freearhey/core'
|
||||
|
||||
export type DataProcessorData = {
|
||||
export interface DataProcessorData {
|
||||
guideChannelsGroupedByStreamId: Dictionary
|
||||
feedsGroupedByChannelId: Dictionary
|
||||
guidesGroupedByStreamId: Dictionary
|
||||
logosGroupedByChannelId: Dictionary
|
||||
logosGroupedByStreamId: Dictionary
|
||||
feedsKeyByStreamId: Dictionary
|
||||
streamsGroupedById: Dictionary
|
||||
channelsKeyById: Dictionary
|
||||
guideChannels: Collection
|
||||
channels: Collection
|
||||
streams: Collection
|
||||
guides: Collection
|
||||
feeds: Collection
|
||||
logos: Collection
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Collection } from '@freearhey/core'
|
||||
|
||||
export type FeedData = {
|
||||
export interface FeedData {
|
||||
channel: string
|
||||
id: string
|
||||
name: string
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
export type GuideData = {
|
||||
export interface GuideData {
|
||||
channel: string
|
||||
feed: string
|
||||
site: string
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
export interface LogoData {
|
||||
channel: string
|
||||
feed: string | null
|
||||
tags: string[]
|
||||
width: number
|
||||
height: number
|
||||
format: string | null
|
||||
url: string
|
||||
}
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
export type StreamData = {
|
||||
export interface StreamData {
|
||||
channel: string | null
|
||||
feed: string | null
|
||||
name?: string
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./9tv.co.il.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')
|
||||
@@ -18,8 +20,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. </div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||
const result = parser({ content, date }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -49,7 +50,7 @@ it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: '<!DOCTYPE html><html><head></head><body></body></html>'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. </div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html><head></head><body></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||
@@ -2,6 +2,8 @@ const { parser, url } = require('./allente.dk.config.js')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
const { readFileSync } = require('fs')
|
||||
const { resolve } = require('path')
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(utc)
|
||||
|
||||
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
||||
const content = readFileSync(resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content, channel }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./allente.fi.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')
|
||||
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content, channel }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -44,7 +45,7 @@ it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./allente.no.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')
|
||||
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content, channel }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -44,7 +45,7 @@ it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"date":"2001-11-17","categories":[],"channels":[]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./allente.se.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')
|
||||
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content, channel }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -44,7 +45,7 @@ it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<channels>
|
||||
<channel site="arianaafgtv.com" lang="en" xmltv_id="ArianaAfghanistanInternationalTV.us" site_id="#">Ariana Afghanistan Television</channel>
|
||||
</channels>
|
||||
@@ -1,82 +0,0 @@
|
||||
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: 'arianaafgtv.com',
|
||||
days: 2,
|
||||
url: 'https://www.arianaafgtv.com/index.html',
|
||||
parser({ content, date }) {
|
||||
const programs = []
|
||||
const items = parseItems(content, date)
|
||||
items.forEach(item => {
|
||||
const title = item.title
|
||||
const start = parseStart(item, date)
|
||||
const stop = parseStop(item, date)
|
||||
programs.push({
|
||||
title,
|
||||
start,
|
||||
stop
|
||||
})
|
||||
})
|
||||
|
||||
return programs
|
||||
}
|
||||
}
|
||||
|
||||
function parseStop(item, date) {
|
||||
const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}`
|
||||
|
||||
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
|
||||
}
|
||||
|
||||
function parseStart(item, date) {
|
||||
const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}`
|
||||
|
||||
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
|
||||
}
|
||||
|
||||
function parseItems(content, date) {
|
||||
const $ = cheerio.load(content)
|
||||
const dayOfWeek = date.format('dddd')
|
||||
const column = $('.H4')
|
||||
.filter((i, el) => {
|
||||
return $(el).text() === dayOfWeek
|
||||
})
|
||||
.first()
|
||||
.parent()
|
||||
|
||||
const rows = column
|
||||
.find('.Paragraph')
|
||||
.map((i, el) => {
|
||||
return $(el).html()
|
||||
})
|
||||
.toArray()
|
||||
.map(r => (r === ' ' ? '|' : r))
|
||||
.join(' ')
|
||||
.split('|')
|
||||
|
||||
const items = []
|
||||
rows.forEach(row => {
|
||||
row = row.trim()
|
||||
if (row) {
|
||||
const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi)
|
||||
if (!found) return
|
||||
const time = found[0]
|
||||
let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1]
|
||||
start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
|
||||
let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1]
|
||||
end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
|
||||
const title = row.replace(time, '').replace(' ', '').trim()
|
||||
items.push({ start, end, title })
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# arianaafgtv.com
|
||||
|
||||
https://arianaafgtv.com/#ariana-afghanistan-television-tv-guide
|
||||
|
||||
### Download the guide
|
||||
|
||||
```sh
|
||||
npm run grab --- --site=arianaafgtv.com
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```sh
|
||||
npm test --- arianaafgtv.com
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./arianatelevision.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')
|
||||
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||
const result = parser({ content, date }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -52,8 +53,7 @@ it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content:
|
||||
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url, request } = require('./artonline.tv.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')
|
||||
@@ -37,8 +39,7 @@ it('can generate valid request data for tomorrow', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./chada.ma.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
@@ -10,30 +12,12 @@ 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 content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||
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')
|
||||
@@ -52,7 +36,7 @@ it('can parse response', () => {
|
||||
|
||||
it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
content: '<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"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.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"code":60,"message":"Resource not found","param":{},"description":"L'URI demandé ou la ressource demandée n'existe pas.","stackTrace":null}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"date":"2024-12-26","categories":[],"channels":[]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./cosmotetv.gr.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')
|
||||
@@ -13,29 +15,6 @@ 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()
|
||||
@@ -45,7 +24,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content = JSON.stringify(mockEpgData)
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
|
||||
const result = parser({ date, content }).map(p => {
|
||||
p.start = dayjs(p.start).toISOString()
|
||||
p.stop = dayjs(p.stop).toISOString()
|
||||
@@ -70,7 +49,7 @@ it('can handle empty guide', () => {
|
||||
const result = parser({
|
||||
date,
|
||||
channel,
|
||||
content: '{"date":"2024-12-26","categories":[],"channels":[]}'
|
||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<channels>
|
||||
<channel site="ctc.ru" lang="ru" xmltv_id="STS.ru" site_id="ctc">STS</channel>
|
||||
</channels>
|
||||
@@ -0,0 +1,95 @@
|
||||
const dayjs = require('dayjs')
|
||||
|
||||
module.exports = {
|
||||
site: 'ctc.ru',
|
||||
days: 1,
|
||||
url: ({ date }) => `https://ctc.ru/api/page/v2/programm/?date=${formatDate(date)}`,
|
||||
parser({ content }) {
|
||||
const programs = []
|
||||
const items = parseItems(content)
|
||||
for (const item of items) {
|
||||
programs.push({
|
||||
title: item.bubbleTitle,
|
||||
// more like "films", "shows", "cartoons" - not a genre
|
||||
category: item.bubbleSubTitle,
|
||||
icons: parseIcons(item),
|
||||
images: parseImages(item),
|
||||
start: parseStart(item),
|
||||
stop: parseStop(item),
|
||||
// not sure if CTC uses this more like `premiere` but I don't have any
|
||||
// additional info to use in the `premiere` field so I'm using this
|
||||
// instead.
|
||||
new: item.isPremiere ?? false,
|
||||
url: item.bubbleUrl ? `https://ctc.ru${item.bubbleUrl}` : undefined,
|
||||
rating: parseRating(item),
|
||||
})
|
||||
}
|
||||
|
||||
return programs
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return dayjs(date).format('DD-MM-YYYY')
|
||||
}
|
||||
|
||||
function parseIcons(item) {
|
||||
const images = item.bubbleImage ?? []
|
||||
// biggest first
|
||||
const sorted = images.sort((a, b) => b.height - a.height)
|
||||
|
||||
return sorted.map((image) => ({
|
||||
src: image.url,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
}))
|
||||
}
|
||||
|
||||
function parseImages(item) {
|
||||
const images = item.trackImageUrl ?? []
|
||||
// biggest first
|
||||
const sorted = images.sort((a, b) => b.height - a.height)
|
||||
|
||||
// compile one image of each size since the content should be the same
|
||||
const sizes = {}
|
||||
for (const image of sorted) {
|
||||
const maxRes = Math.max(image.width, image.height)
|
||||
const item = {
|
||||
type: 'backdrop',
|
||||
// https://github.com/ektotv/xmltv/blob/801417b4b7aae38f13caa82d8bbfbed0a254ee5f/src/types.ts#L40-L48
|
||||
size: maxRes > 400 ? '3' : maxRes > 200 ? '2' : '1',
|
||||
orient: image.height > image.width ? 'P' : 'L',
|
||||
value: image.url,
|
||||
}
|
||||
if (sizes[item.size]) continue
|
||||
sizes[item.size] = item
|
||||
}
|
||||
|
||||
// re-sort so the size 3 images are first
|
||||
return Object
|
||||
.values(sizes)
|
||||
.sort((a, b) => Number(b.size) - Number(a.size))
|
||||
}
|
||||
|
||||
function parseStart(item) {
|
||||
return dayjs(item.startTime)
|
||||
}
|
||||
|
||||
function parseStop(item) {
|
||||
return dayjs(item.endTime)
|
||||
}
|
||||
|
||||
function parseRating(item) {
|
||||
if (item.ageLimit == null) return null
|
||||
return {
|
||||
// Not sure what the Russian system is actually called (if anything)
|
||||
system: 'Russia',
|
||||
value: `${item.ageLimit}+`,
|
||||
}
|
||||
}
|
||||
|
||||
function parseItems(content) {
|
||||
const node = JSON.parse(content).content.find(n => n.type === 'tv-program')
|
||||
if (node) return node.widgets
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const { parser, url } = require('./ctc.ru.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('2025-06-22')
|
||||
|
||||
it('can generate valid url', () => {
|
||||
expect(url({ date })).toBe('https://ctc.ru/api/page/v2/programm/?date=22-06-2025')
|
||||
})
|
||||
|
||||
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: '2025-06-22T03:00:00.000Z',
|
||||
stop: '2025-06-22T03:55:00.000Z',
|
||||
title: 'Три кота',
|
||||
category: 'Мультфильмы',
|
||||
new: false,
|
||||
url: 'https://ctc.ru/collections/multiki/',
|
||||
icons: [
|
||||
{
|
||||
src: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-dictionary-category/5/iconurl/web/5f9027fcba9ac-125x125.png',
|
||||
width: 125,
|
||||
height: 125,
|
||||
},
|
||||
{
|
||||
src: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-dictionary-category/5/iconurl/web/5f9027fc99587-60x60.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
type: 'backdrop',
|
||||
size: '3',
|
||||
orient: 'L',
|
||||
value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f0a31b5-1740x978.jpeg',
|
||||
},
|
||||
{
|
||||
type: 'backdrop',
|
||||
size: '2',
|
||||
orient: 'L',
|
||||
value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f20bac2-400x225.jpeg',
|
||||
},
|
||||
{
|
||||
type: 'backdrop',
|
||||
size: '1',
|
||||
orient: 'L',
|
||||
value: 'https://mgf-static-ssl.ctc.ru/images/ctc-entity-project/1129/horizontalcover/web/66c319f244f92-150x84.jpeg',
|
||||
},
|
||||
],
|
||||
rating: {
|
||||
system: 'Russia',
|
||||
value: '0+',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('can handle empty guide', () => {
|
||||
const results = parser({
|
||||
content: JSON.stringify({
|
||||
isActive: true,
|
||||
url: '/programm/',
|
||||
header: [],
|
||||
sidebar: [],
|
||||
footer: [],
|
||||
content: [],
|
||||
seoTags: {},
|
||||
ogMarkup: {},
|
||||
userGeo: null,
|
||||
userData: null,
|
||||
meta: {},
|
||||
activeFrom: null,
|
||||
activeTo: null,
|
||||
type: 'tv-program-page',
|
||||
})
|
||||
})
|
||||
expect(results).toMatchObject([])
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
# ctc.ru
|
||||
|
||||
https://ctc.ru/programm
|
||||
|
||||
### Download the guide
|
||||
|
||||
```sh
|
||||
npm run grab --- --site=ctc.ru
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```sh
|
||||
npm test --- ctc.ru
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { url, parser } = require('./cubmu.com.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
@@ -14,9 +16,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
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 content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
|
||||
const idResults = parser({ content, channel })
|
||||
expect(idResults).toMatchObject([
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ const utc = require('dayjs/plugin/utc')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
const parseDuration = require('parse-duration').default
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
const _ = require('lodash')
|
||||
const sortBy = require('lodash.sortby')
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(utc)
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
}
|
||||
})
|
||||
|
||||
return _.sortBy(programs, p => p.start)
|
||||
return sortBy(programs, p => p.start.valueOf())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url, request } = require('./directv.com.ar.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')
|
||||
@@ -45,8 +47,7 @@ it('can generate valid request data', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content, channel }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
|
||||
@@ -3,6 +3,7 @@ const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
const uniqBy = require('lodash.uniqby')
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
@@ -45,7 +46,6 @@ module.exports = {
|
||||
return programs
|
||||
},
|
||||
async channels({ country }) {
|
||||
const _ = require('lodash')
|
||||
|
||||
const countries = {
|
||||
ao: 'ago',
|
||||
@@ -114,7 +114,7 @@ module.exports = {
|
||||
})
|
||||
})
|
||||
|
||||
return _.uniqBy(channels, 'site_id')
|
||||
return uniqBy(channels, 'site_id')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,19 +107,19 @@ https://epgshare01.online/epgshare01/
|
||||
Windows (Command Prompt):
|
||||
|
||||
```sh
|
||||
SET "NODE_OPTIONS=--max-old-space-size=5000" && npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_<TAG>.channels.xml
|
||||
SET "NODE_OPTIONS=--max-old-space-size=6000" && npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_<TAG>.channels.xml
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```sh
|
||||
$env:NODE_OPTIONS="--max-old-space-size=5000"; npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_<TAG>.channels.xml
|
||||
$env:NODE_OPTIONS="--max-old-space-size=6000"; npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_<TAG>.channels.xml
|
||||
```
|
||||
|
||||
Linux and macOS:
|
||||
|
||||
```sh
|
||||
NODE_OPTIONS=--max-old-space-size=5000 npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_<TAG>.channels.xml
|
||||
NODE_OPTIONS=--max-old-space-size=6000 npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_<TAG>.channels.xml
|
||||
```
|
||||
|
||||
### Update channel list
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"data":{"entries":{"243":[{"createdAt":"2023-11-05T17:09:34.000Z","updatedAt":"2023-11-05T17:09:34.000Z","id":"009f3a34-8164-4ff9-b981-9dcab1a518fc","channelNo":"243","programmeId":null,"title":"News Live","episode":null,"slug":"news-live","date":"2023-11-08 17:00:00","startTime":"2023-11-08 20:00:00","endTime":"2023-11-08 20:30:00","length":1800,"description":"News Live","long_description":"Up to date news and analysis from around the world.","status":true,"channel":{"id":"7fd7a9a6-af32-c861-d2b0-4ddc7846fad2","key":"AljaInt","no":243,"name":"Al Jazeera International","slug":"al-jazeera-international","website":null,"description":"<p>An international 24-hour English-language It is the first English-language news channel brings you the latest global news stories, analysis from the Middle East & worldwide.</p>","shortDescription":null,"logo":"files/logos/channels/11-NEWS/AlJazeera Int SD-FirstMedia-Chl-243.jpg","externalId":"132","type":"radio","status":true,"chanel":"SD","locale":"id","relationId":"5a6ea4ae-a008-4889-9c68-7a6f1838e81d","onlyfm":null,"genress":[{"id":"1db3bb43-b00d-49af-b272-6c058a8c0b49","name":"International Free View"},{"id":"2e81a4bd-9719-4186-820a-7e035e07be13","name":"News"}]}}]}}}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { url, parser } = require('./firstmedia.com.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
@@ -13,8 +15,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"data":{"entries":{"243":[{"createdAt":"2023-11-05T17:09:34.000Z","updatedAt":"2023-11-05T17:09:34.000Z","id":"009f3a34-8164-4ff9-b981-9dcab1a518fc","channelNo":"243","programmeId":null,"title":"News Live","episode":null,"slug":"news-live","date":"2023-11-08 17:00:00","startTime":"2023-11-08 20:00:00","endTime":"2023-11-08 20:30:00","length":1800,"description":"News Live","long_description":"Up to date news and analysis from around the world.","status":true,"channel":{"id":"7fd7a9a6-af32-c861-d2b0-4ddc7846fad2","key":"AljaInt","no":243,"name":"Al Jazeera International","slug":"al-jazeera-international","website":null,"description":"<p>An international 24-hour English-language It is the first English-language news channel brings you the latest global news stories, analysis from the Middle East & worldwide.</p>","shortDescription":null,"logo":"files/logos/channels/11-NEWS/AlJazeera Int SD-FirstMedia-Chl-243.jpg","externalId":"132","type":"radio","status":true,"chanel":"SD","locale":"id","relationId":"5a6ea4ae-a008-4889-9c68-7a6f1838e81d","onlyfm":null,"genress":[{"id":"1db3bb43-b00d-49af-b272-6c058a8c0b49","name":"International Free View"},{"id":"2e81a4bd-9719-4186-820a-7e035e07be13","name":"News"}]}}]}}}'
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
|
||||
const results = parser({ content, channel, date })
|
||||
|
||||
// All time in Asia/Jakarta
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"channel-programme":[
|
||||
{
|
||||
"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07",
|
||||
"programmeTitle":"NRL",
|
||||
"title":"Eels v Titans",
|
||||
"startTime":"2022-12-14T00:00:00+11:00",
|
||||
"endTime":"2022-12-14T01:00:00+11:00",
|
||||
"duration":60,
|
||||
"live":false,
|
||||
"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559",
|
||||
"channelId":2,
|
||||
"channelName":"FOX League",
|
||||
"channelAbbreviation":"LEAGUE",
|
||||
"programmeUID":235220,
|
||||
"round":"R1",
|
||||
"statsMatchId":null,
|
||||
"closedCaptioned":true,
|
||||
"statsFixtureId":10207,
|
||||
"genreTitle":"Rugby League",
|
||||
"parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11",
|
||||
"parentGenreTitle":"Sport",
|
||||
"pmgId":"PMG01306944",
|
||||
"statsSport":"league",
|
||||
"type":"GAME",
|
||||
"hiDef":true,
|
||||
"widescreen":true,
|
||||
"classification":"",
|
||||
"synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.",
|
||||
"preGameStartTime":null,
|
||||
"closeCaptioned":true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"channel-programme":[]}
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./foxsports.com.au.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
@@ -15,9 +17,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content =
|
||||
'{"channel-programme":[{"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07","programmeTitle":"NRL","title":"Eels v Titans","startTime":"2022-12-14T00:00:00+11:00","endTime":"2022-12-14T01:00:00+11:00","duration":60,"live":false,"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559","channelId":2,"channelName":"FOX League","channelAbbreviation":"LEAGUE","programmeUID":235220,"round":"R1","statsMatchId":null,"closedCaptioned":true,"statsFixtureId":10207,"genreTitle":"Rugby League","parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11","parentGenreTitle":"Sport","pmgId":"PMG01306944","statsSport":"league","type":"GAME","hiDef":true,"widescreen":true,"classification":"","synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.","preGameStartTime":null,"closeCaptioned":true}]}'
|
||||
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const result = parser({ content, channel }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
@@ -38,11 +38,6 @@ it('can parse response', () => {
|
||||
})
|
||||
|
||||
it('can handle empty guide', () => {
|
||||
const result = parser(
|
||||
{
|
||||
content: '{"channel-programme":[]}'
|
||||
},
|
||||
channel
|
||||
)
|
||||
const result = parser({content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))}, channel)
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[{"id":5856194,"publicUid":"f17b5a1d-3f2a-42a0-a11b-a08e5c9785fd","title":"בוש 4 - פרק 3","lead":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","description":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","rating":14,"ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T21:26:00Z","till":"2025-03-27T22:17:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/bw-4---prq-3,5856194","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T21:26:00Z","npvrTill":"2025-04-10T21:26:00Z","programRecordingId":7068534,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"bw-4---prq-3"},{"id":5858584,"publicUid":"1dc296c9-0c28-4202-8b20-cbef56a763f5","title":"אבא משתדל - 5. חבר","lead":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","description":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T22:17:00Z","till":"2025-03-27T22:43:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/b-mdl---5-br,5858584","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T22:17:00Z","npvrTill":"2025-04-10T22:17:00Z","programRecordingId":7071052,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"b-mdl---5-br"}]
|
||||
@@ -1,4 +1,6 @@
|
||||
const { parser, url } = require('./freetv.tv.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')
|
||||
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
|
||||
})
|
||||
|
||||
it('can parse response', () => {
|
||||
const content = '[{"id":5856194,"publicUid":"f17b5a1d-3f2a-42a0-a11b-a08e5c9785fd","title":"בוש 4 - פרק 3","lead":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","description":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","rating":14,"ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T21:26:00Z","till":"2025-03-27T22:17:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/bw-4---prq-3,5856194","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T21:26:00Z","npvrTill":"2025-04-10T21:26:00Z","programRecordingId":7068534,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"bw-4---prq-3"},{"id":5858584,"publicUid":"1dc296c9-0c28-4202-8b20-cbef56a763f5","title":"אבא משתדל - 5. חבר","lead":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","description":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T22:17:00Z","till":"2025-03-27T22:43:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/b-mdl---5-br,5858584","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T22:17:00Z","npvrTill":"2025-04-10T22:17:00Z","programRecordingId":7071052,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"b-mdl---5-br"}]'
|
||||
|
||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||
const results = parser({ content }).map(p => {
|
||||
p.start = p.start.toJSON()
|
||||
p.stop = p.stop.toJSON()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user