diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 8549e92c..610e70d2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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' diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 311edb68..9975722d 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -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' diff --git a/README.md b/README.md index 69fb91a8..c494b704 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Options: --days Number of days for which the program will be loaded (defaults to the value from the site config) --maxConnections 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) | diff --git a/SITES.md b/SITES.md index 8d34b04b..4fb9e1d9 100644 --- a/SITES.md +++ b/SITES.md @@ -73,7 +73,7 @@ guidetnt.com6969🟢 horizon.tv184172🟢 hoy.tv31🟢 - i.mjh.nz64581488🟢 + i.mjh.nz64581489🟢 i24news.tv43🟢 iltalehti.fi14244🟢 indihometv.com130124🟢 @@ -151,7 +151,7 @@ singtel.com155113🟢 sjonvarp.is1313🟢 sky.co.nz11193🟢 - sky.com559458🟢https://github.com/iptv-org/epg/issues/2763 + sky.com559458🟡https://github.com/iptv-org/epg/issues/2763 sky.de7575🟢 skylife.co.kr2510🟢 skyperfectv.co.jp137130🟢 @@ -180,7 +180,7 @@ tv-spored.siol.net3120🟢 tv.blue.ch1030565🟢 tv.cctv.com9488🟢 - tv.dir.bg11193🟢https://github.com/iptv-org/epg/issues/2779 + tv.dir.bg11193🔴https://github.com/iptv-org/epg/issues/2779 tv.lv13749🟢 tv.magenta.at307228🟢 tv.mail.ru664643🟢 @@ -189,7 +189,7 @@ tv.post.lu332242🟢 tv.sfr.fr489456🟢 tv.trueid.net26674🟢 - tv.yandex.ru9767🟢https://github.com/iptv-org/epg/issues/2803 + tv.yandex.ru9767🔴https://github.com/iptv-org/epg/issues/2803 tv24.co.uk107239🟢 tv24.se326157🟢 tv2go.t-2.net335254🟢 @@ -211,11 +211,11 @@ tvmusor.hu9967🟢 tvmustra.hu1880🟢 tvpassport.com192872509🟢 - tvplus.com.tr143134🟢 + tvplus.com.tr143134🟢https://github.com/iptv-org/epg/issues/2816 tvprofil.com5836455🟢 tvtv.us22992255🟢 v3.myafn.dodmedia.osd.mil88🟢 - vidio.com4746🟢 + vidio.com5752🟢 virginmediatelevision.ie55🟢 virgintvgo.virginmedia.com238195🟢 visionplus.id250226🟢 diff --git a/package-lock.json b/package-lock.json index 34a76856..05218b45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@alex_neo/jest-expect-message": "^1.0.5", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.31.0", + "@freearhey/chronos": "^0.0.1", "@freearhey/core": "^0.10.2", "@freearhey/search-js": "^0.1.2", "@ntlab/sfetch": "^1.2.0", @@ -601,458 +602,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/core/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/win32-x64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", @@ -1200,6 +749,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@freearhey/chronos": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@freearhey/chronos/-/chronos-0.0.1.tgz", + "integrity": "sha512-nzu26ct0BMzDZ+7QGOve5kMPMayY4VoipCrCcVQ06x3ULIieV9oMbOMFjFVENCoevDGJ7telMsK84F0SpUgxnQ==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0", + "node-cron": "^4.2.1", + "string-argv": "^0.3.2" + }, + "bin": { + "chronos": "index.js" + } + }, "node_modules/@freearhey/core": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.10.2.tgz", @@ -2545,18 +2108,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2728,97 +2279,6 @@ "@octokit/openapi-types": "^25.1.0" } }, - "node_modules/@oxlint/darwin-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.7.0.tgz", - "integrity": "sha512-51vhCSQO4NSkedwEwOyqThiYqV0DAUkwNdqMQK0d29j5zmtNJJJRRBLeQuLGdstNmn3F7WMQ75Ci0/3Nq4ff8A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint/darwin-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.7.0.tgz", - "integrity": "sha512-c0GN52yehYZ4TYuh4lBH9wYbBOI/RDOxZhJdBsttG0GwfvKYg/tiPNrNEsPzu0/rd1j6x3yT0zt6vezDMeC1sQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint/linux-arm64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.7.0.tgz", - "integrity": "sha512-pam/lbzbzVMDzc3f1hoRPtnUMEIqkn0dynlB5nUll/MVBSIvIPLS9kJLrRA48lrlqbkS9LGiF37JvpwXA58A9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-arm64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.7.0.tgz", - "integrity": "sha512-LTyPy9FYS3SZ2XxJx+ITvlAq/ek5PtZK9Z2m3W72TA8hchGhJy5eQ+aotYjd/YVXOpGRpB12RdOpOTsZRu50bA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-x64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.7.0.tgz", - "integrity": "sha512-YtZ4DiAgjaEiqUiwnvtJ/znZMAAVPKR7pnsi6lqbA3BfXJ/IwMaNpdoGlCGVdDGeN4BuGCwnFtBVqKVvVg3DDg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/linux-x64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.7.0.tgz", - "integrity": "sha512-5aIpemNUBvwMMk4MCx1V3M6R9eMB1/SS6/24Orax9FqaI1lDX08tySdv696sr4Lms9ocA+rotxIPW9NP9439vA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint/win32-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.7.0.tgz", - "integrity": "sha512-fpFpkHwbAu0NcR5bc1WapCPcM9qSYi5lCRVOp1WwDoFLKI2b9/UWB8OEg8UHWV5dnBu7HZAWH/SEslYGkZNsbQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@oxlint/win32-x64": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.7.0.tgz", @@ -3201,150 +2661,6 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.1.tgz", - "integrity": "sha512-zO6SW/jSMTUORPm6dUZFPUwf+EFWZsaXWMGXadRG6akCofYpoQb8pcY2QZkVr43z8TMka6BtXpyoD/DJ0iOPHQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.1.tgz", - "integrity": "sha512-8RjaTZYxrlYKE5PgzZYWSOT4mAsyhIuh30Nu4dnn/2r0Ef68iNCbvX4ynGnFMhOIhqunjQbJf+mJKpwTwdHXhw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.1.tgz", - "integrity": "sha512-jEqK6pECs2m4BpL2JA/4CCkq04p6iFOEtVNXTisO+lJ3zwmxlnIEm9UfJZG6VSu8GS9MHRKGB0ieZ1tEdN1qDA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.1.tgz", - "integrity": "sha512-PbkuIOYXO/gQbWQ7NnYIwm59ygNqmUcF8LBeoKvxhx1VtOwE+9KiTfoplOikkPLhMiTzKsd8qentTslbITIg+Q==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.1.tgz", - "integrity": "sha512-JaqFdBCarIBKiMu5bbAp+kWPMNGg97ej+7KzbKOzWP5pRptqKi86kCDZT3WmjPe8hNG6dvBwbm7Y8JNry5LebQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.1.tgz", - "integrity": "sha512-t4cLkku10YECDaakWUH0452WJHIZtrLPRwezt6BdoMntVMwNjvXRX7C8bGuYcKC3YxRW7enZKFpozLhQIQ37oA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.1.tgz", - "integrity": "sha512-fSMwZOaG+3ukUucbEbzz9GhzGhUhXoCPqHe9qW0/Vc2IZRp538xalygKyZynYweH5d9EHux1aj3+IO8/xBaoiA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.1.tgz", - "integrity": "sha512-tweCXK/79vAwj1NhAsYgICy8T1z2QEairmN2BFEBYFBFNMEB1iI1YlXwBkBtuihRvgZrTh1ORusKa4jLYzLCZA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.1.tgz", - "integrity": "sha512-zi7hO9D+2R2yQN9D7T10/CAI9KhuXkNkz8tcJOW6+dVPtAk/gsIC5NoGPELjgrAlLL9CS38ZQpLDslLfpP15ng==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-win32-x64-msvc": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.1.tgz", @@ -3399,23 +2715,6 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tybys/wasm-util/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3867,243 +3166,6 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", @@ -6586,20 +5648,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -9417,6 +8465,15 @@ "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", "license": "MIT" }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-ensure": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", @@ -11064,6 +10121,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", diff --git a/package.json b/package.json index af6070bc..84c90133 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "prepare": "husky" }, "private": true, - "author": "Arhey", "license": "UNLICENSED", "jest": { "setupFiles": [ @@ -40,6 +39,7 @@ "@alex_neo/jest-expect-message": "^1.0.5", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.31.0", + "@freearhey/chronos": "^0.0.1", "@freearhey/core": "^0.10.2", "@freearhey/search-js": "^0.1.2", "@ntlab/sfetch": "^1.2.0", diff --git a/pm2.config.js b/pm2.config.js index 67c85e77..8fc9bc3c 100644 --- a/pm2.config.js +++ b/pm2.config.js @@ -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 } ] } diff --git a/scripts/commands/api/generate.ts b/scripts/commands/api/generate.ts index b43bc84b..b0e078c4 100644 --- a/scripts/commands/api/generate.ts +++ b/scripts/commands/api/generate.ts @@ -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,31 +12,24 @@ async function main() { logger.info('loading channels...') const sitesStorage = new Storage(SITES_DIR) - const parser = new ChannelsParser({ storage: sitesStorage }) + const parser = new ChannelsParser({ + storage: sitesStorage + }) - let files: string[] = [] - files = await sitesStorage.list('**/*.channels.xml') + const files: string[] = await sitesStorage.list('**/*.channels.xml') - let parsedChannels = new Collection() + const channels = new Collection() for (const filepath of files) { - parsedChannels = parsedChannels.concat(await parser.parse(filepath)) + const channelList = await parser.parse(filepath) + + channelList.channels.forEach((data: epgGrabber.Channel) => { + channels.add(new GuideChannel(data)) + }) } - logger.info(` found ${parsedChannels.count()} channel(s)`) + logger.info(`found ${channels.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 output = channels.map((channel: GuideChannel) => channel.toJSON()) const apiStorage = new Storage(API_DIR) const outputFilename = 'guides.json' diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts index 9e731c7f..7a8f753d 100644 --- a/scripts/commands/api/load.ts +++ b/scripts/commands/api/load.ts @@ -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') ]) } diff --git a/scripts/commands/channels/edit.ts b/scripts/commands/channels/edit.ts index a67b6fe2..4a5e714a 100644 --- a/scripts/commands/channels/edit.ts +++ b/scripts/commands/channels/edit.ts @@ -1,17 +1,17 @@ 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 } @@ -34,11 +34,11 @@ program.argument('', '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`) } diff --git a/scripts/commands/channels/lint.mts b/scripts/commands/channels/lint.mts index 8d4bb161..72cb003c 100644 --- a/scripts/commands/channels/lint.mts +++ b/scripts/commands/channels/lint.mts @@ -19,6 +19,7 @@ const xsd = ` + diff --git a/scripts/commands/channels/parse.ts b/scripts/commands/channels/parse.ts index 572b5ed6..fb0e0447 100644 --- a/scripts/commands/channels/parse.ts +++ b/scripts/commands/channels/parse.ts @@ -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 @@ -21,17 +22,25 @@ type ParseOptions = { const options: ParseOptions = program.opts() async function main() { + function isPromise(promise: object[] | Promise) { + return ( + !!promise && + typeof promise === 'object' && + typeof (promise as Promise).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: { @@ -49,45 +58,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) { - return ( - !!promise && - typeof promise === 'object' && - typeof (promise as Promise).then === 'function' - ) -} diff --git a/scripts/commands/channels/validate.ts b/scripts/commands/channels/validate.ts index 27c5fabb..43af23e5 100644 --- a/scripts/commands/channels/validate.ts +++ b/scripts/commands/channels/validate.ts @@ -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) type ValidationError = { - type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang' + type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang' name: string lang?: string xmltv_id?: string @@ -19,12 +21,14 @@ 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 @@ -35,11 +39,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) @@ -54,12 +58,21 @@ async function main() { } 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 }) + errors.push({ type: 'wrong_channel_id', ...channel }) totalErrors++ } + + if (feedId) { + const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id) + if (!foundFeed) { + errors.push({ type: 'wrong_feed_id', ...channel }) + totalErrors++ + } + } }) if (errors.length) { diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index 7d4e0bb9..ebf94e70 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -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 of the site to parse')) @@ -44,6 +45,7 @@ program .default(false) .env('GZIP') ) + .addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL')) .parse() export type GrabOptions = { @@ -51,6 +53,7 @@ export type GrabOptions = { 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 }) diff --git a/scripts/commands/sites/update.ts b/scripts/commands/sites/update.ts index a2cf5cd7..42db9acf 100644 --- a/scripts/commands/sites/update.ts +++ b/scripts/commands/sites/update.ts @@ -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: `${site.domain}` }, { 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
(total / with xmltv-id)', colspan: 2, align: 'left' }, { name: 'Status', align: 'left' }, diff --git a/scripts/core/channelsParser.ts b/scripts/core/channelsParser.ts index d4630506..43a5f28b 100644 --- a/scripts/core/channelsParser.ts +++ b/scripts/core/channelsParser.ts @@ -1,5 +1,6 @@ import { parseChannels } from 'epg-grabber' -import { Storage, Collection } from '@freearhey/core' +import { Storage } from '@freearhey/core' +import { ChannelList } from '../models' type 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 { 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 }) } } diff --git a/scripts/core/dataLoader.ts b/scripts/core/dataLoader.ts index 51348bba..3d817977 100644 --- a/scripts/core/dataLoader.ts +++ b/scripts/core/dataLoader.ts @@ -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 } } diff --git a/scripts/core/dataProcessor.ts b/scripts/core/dataProcessor.ts index 372d8716..1f0252fc 100644 --- a/scripts/core/dataProcessor.ts +++ b/scripts/core/dataProcessor.ts @@ -1,6 +1,6 @@ +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() {} @@ -9,31 +9,48 @@ export class DataProcessor { 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 } } } diff --git a/scripts/core/grabber.ts b/scripts/core/grabber.ts index 2248cbc5..57bd322d 100644 --- a/scripts/core/grabber.ts +++ b/scripts/core/grabber.ts @@ -70,6 +70,10 @@ export class Grabber { } } + if (this.options.curl === true) { + config.curl = true + } + const _programs = await this.grabber.grab( channel, date, diff --git a/scripts/core/guide.ts b/scripts/core/guide.ts deleted file mode 100644 index 924f79f3..00000000 --- a/scripts/core/guide.ts +++ /dev/null @@ -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(), { - timezone: '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 = 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) - } - } -} diff --git a/scripts/core/guideManager.ts b/scripts/core/guideManager.ts index 78640a32..aee2f666 100644 --- a/scripts/core/guideManager.ts +++ b/scripts/core/guideManager.ts @@ -1,7 +1,12 @@ -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 = { options: OptionValues @@ -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) } } } diff --git a/scripts/core/index.ts b/scripts/core/index.ts index f545c6c2..8d528fe7 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -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' diff --git a/scripts/core/issueLoader.ts b/scripts/core/issueLoader.ts index 4aa1cceb..eebd6c39 100644 --- a/scripts/core/issueLoader.ts +++ b/scripts/core/issueLoader.ts @@ -23,6 +23,7 @@ export class IssueLoader { repo: REPO, per_page: 100, labels, + state: 'open', headers: { 'X-GitHub-Api-Version': '2022-11-28' } diff --git a/scripts/core/proxyParser.ts b/scripts/core/proxyParser.ts index 244290d5..3e316ab2 100644 --- a/scripts/core/proxyParser.ts +++ b/scripts/core/proxyParser.ts @@ -2,9 +2,9 @@ import { URL } from 'node:url' type 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 } } diff --git a/scripts/core/queueCreator.ts b/scripts/core/queueCreator.ts index a09632e8..71213630 100644 --- a/scripts/core/queueCreator.ts +++ b/scripts/core/queueCreator.ts @@ -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 = { 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 { - 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 } diff --git a/scripts/core/xml.ts b/scripts/core/xml.ts deleted file mode 100644 index fd42f993..00000000 --- a/scripts/core/xml.ts +++ /dev/null @@ -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 = '\r\n\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 += ` ${escapeString(channel.name)}\r\n` - }) - - output += '\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(/\n|\r/g, ' ') - .replace(/ +/g, ' ') - .trim() -} diff --git a/scripts/core/xmltv.ts b/scripts/core/xmltv.ts deleted file mode 100644 index 5603c4e7..00000000 --- a/scripts/core/xmltv.ts +++ /dev/null @@ -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() - }) - } -} diff --git a/scripts/models/channel.ts b/scripts/models/channel.ts index 2fb734bd..fb62a3fc 100644 --- a/scripts/models/channel.ts +++ b/scripts/models/channel.ts @@ -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: boolean = 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: { [key: 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() } diff --git a/scripts/models/channelList.ts b/scripts/models/channelList.ts new file mode 100644 index 00000000..d312e71c --- /dev/null +++ b/scripts/models/channelList.ts @@ -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: 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(/\n|\r/g, ' ') + .replace(/ +/g, ' ') + .trim() + } + + let output = '\r\n\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 += ` ${displayName}\r\n` + }) + + output += '\r\n' + + return output + } +} diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts index 0035bb49..bb2c7020 100644 --- a/scripts/models/feed.ts +++ b/scripts/models/feed.ts @@ -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: { [key: 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 || '' + } } diff --git a/scripts/models/guide.ts b/scripts/models/guide.ts index a7a4359f..b6026743 100644 --- a/scripts/models/guide.ts +++ b/scripts/models/guide.ts @@ -1,35 +1,35 @@ -import type { GuideData } from '../types/guide' -import { v4 as uuidv4 } from 'uuid' +import { Collection, DateTime } from '@freearhey/core' +import { generateXMLTV } from 'epg-grabber' + +type 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 uuidv4() + 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() + }) } } diff --git a/scripts/models/guideChannel.ts b/scripts/models/guideChannel.ts new file mode 100644 index 00000000..92aca912 --- /dev/null +++ b/scripts/models/guideChannel.ts @@ -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 || '' + } + } +} diff --git a/scripts/models/index.ts b/scripts/models/index.ts index 7602bede..38ab2027 100644 --- a/scripts/models/index.ts +++ b/scripts/models/index.ts @@ -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' diff --git a/scripts/models/logo.ts b/scripts/models/logo.ts new file mode 100644 index 00000000..d864a3fb --- /dev/null +++ b/scripts/models/logo.ts @@ -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: number = 0 + height: number = 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}` + } +} diff --git a/scripts/types/channel.d.ts b/scripts/types/channel.d.ts index d718c6b4..b1d2237c 100644 --- a/scripts/types/channel.d.ts +++ b/scripts/types/channel.d.ts @@ -15,7 +15,6 @@ export type ChannelData = { closed: string replaced_by: string website: string - logo: string } export type ChannelSearchableData = { diff --git a/scripts/types/dataLoader.d.ts b/scripts/types/dataLoader.d.ts index 41f21a96..135340e9 100644 --- a/scripts/types/dataLoader.d.ts +++ b/scripts/types/dataLoader.d.ts @@ -16,4 +16,5 @@ export type DataLoaderData = { timezones: object | object[] guides: object | object[] streams: object | object[] + logos: object | object[] } diff --git a/scripts/types/dataProcessor.d.ts b/scripts/types/dataProcessor.d.ts index e99fb47e..f158f16e 100644 --- a/scripts/types/dataProcessor.d.ts +++ b/scripts/types/dataProcessor.d.ts @@ -1,12 +1,16 @@ import { Collection, Dictionary } from '@freearhey/core' export type 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 } diff --git a/scripts/types/logo.d.ts b/scripts/types/logo.d.ts new file mode 100644 index 00000000..c77f4799 --- /dev/null +++ b/scripts/types/logo.d.ts @@ -0,0 +1,9 @@ +export type LogoData = { + channel: string + feed: string | null + tags: string[] + width: number + height: number + format: string | null + url: string +} diff --git a/sites/epgshare01.online/readme.md b/sites/epgshare01.online/readme.md index 192d0182..7979cca3 100644 --- a/sites/epgshare01.online/readme.md +++ b/sites/epgshare01.online/readme.md @@ -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_.channels.xml +SET "NODE_OPTIONS=--max-old-space-size=6000" && npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml ``` Windows (PowerShell): ```sh -$env:NODE_OPTIONS="--max-old-space-size=5000"; npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml +$env:NODE_OPTIONS="--max-old-space-size=6000"; npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml ``` Linux and macOS: ```sh -NODE_OPTIONS=--max-old-space-size=5000 npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml +NODE_OPTIONS=--max-old-space-size=6000 npm run grab --- --channels=sites/epgshare01.online/epgshare01.online_.channels.xml ``` ### Update channel list diff --git a/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml b/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml index 8a1baf9c..fc581f59 100644 --- a/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml +++ b/sites/i.mjh.nz/i.mjh.nz_samsung.channels.xml @@ -521,7 +521,7 @@ Pluto TV Paranormal Tennis Channel International PBS History - MovieSphere + MovieSphere INWILD Gusto TV Beano TV diff --git a/sites/vidio.com/__data__/auth.json b/sites/vidio.com/__data__/auth.json new file mode 100644 index 00000000..b7f19c07 --- /dev/null +++ b/sites/vidio.com/__data__/auth.json @@ -0,0 +1 @@ +{"api_key":"eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InR5cGUiOiJhcGlrZXkifSwiZXhwIjoxNzUxNDc4MDQ2fQ.kxcPR76NBdQYIrsUnmBpqzcSmPD-H7b_-I_SjFi1zY4","api_key_expires_at":"2025-07-03T00:40:46+07:00"} \ No newline at end of file diff --git a/sites/vidio.com/__data__/content.json b/sites/vidio.com/__data__/content.json new file mode 100644 index 00000000..5f4a8c29 --- /dev/null +++ b/sites/vidio.com/__data__/content.json @@ -0,0 +1 @@ +{"data":[{"id":"4389878","type":"schedule","attributes":{"title":"Ftv PrimeTime : Cinta Dodol Inilah Yang Membuatku Lengket Padamu","start_time":"2025-06-30T22:57:00+07:00","end_time":"2025-07-01T00:29:00+07:00","state":"replayable","description":"Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik. tayang setiap hari","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796735","type":"video"}}}},{"id":"4391225","type":"schedule","attributes":{"title":"FTV UTAMA \"REMBULAN DI KANDANG SAPI\"","start_time":"2025-07-01T00:30:00+07:00","end_time":"2025-07-01T01:30:00+07:00","state":"replayable","description":"Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik.","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796793","type":"video"}}}},{"id":"4391226","type":"schedule","attributes":{"title":"Solusi","start_time":"2025-07-01T01:30:00+07:00","end_time":"2025-07-01T02:00:00+07:00","state":"replayable","description":"-","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796823","type":"video"}}}},{"id":"4391227","type":"schedule","attributes":{"title":"Buser","start_time":"2025-07-01T02:00:00+07:00","end_time":"2025-07-01T02:30:00+07:00","state":"replayable","description":"-","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796853","type":"video"}}}},{"id":"4391228","type":"schedule","attributes":{"title":"Sinema Dini Hari Babysitter Macho","start_time":"2025-07-01T02:30:00+07:00","end_time":"2025-07-01T03:00:00+07:00","state":"replayable","description":"Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik.","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796878","type":"video"}}}},{"id":"4391324","type":"schedule","attributes":{"title":"Sinetron Dini Hari: Cinta Karena Cinta","start_time":"2025-07-01T03:30:00+07:00","end_time":"2025-07-01T04:00:00+07:00","state":"replayable","description":"Jenar sebelumnya mengira bahwa Mila, kakaknya, tewas dalam sebuah insiden kebakaran 15 tahun lalu. Namun, takdir berkata lain. Gadis tersebut ternyata masih hidup dan diadopsi oleh Ayu. Ia kemudian bertekad untuk pergi mencarinya.","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796943","type":"video"}}}},{"id":"4391229","type":"schedule","attributes":{"title":"Barakallah","start_time":"2025-07-01T04:00:00+07:00","end_time":"2025-07-01T04:23:00+07:00","state":"replayable","description":"Ceramah islami penuh makna. Tayang setiap Selasa-Minggu pukul 04.00 WIB","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8796957","type":"video"}}}},{"id":"4391230","type":"schedule","attributes":{"title":"Liputan 6 Pagi - Live","start_time":"2025-07-01T04:30:00+07:00","end_time":"2025-07-01T06:00:00+07:00","state":"replayable","description":"Mengulas berita terkini yang aktual dan terpercaya. Tayang setiap hari pukul 04.24 WIB","image_landscape_url":"https://thumbor.prod.vidiocdn.com/TU0JVjSw8srrF_L7wqEtyaKj63A=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/tv_program/thumbnail/382358/3bec7e.jpg"},"relationships":{"video":{"data":{"id":"8797080","type":"video"}}}},{"id":"4391231","type":"schedule","attributes":{"title":"Hot Shot","start_time":"2025-07-01T06:00:00+07:00","end_time":"2025-07-01T07:00:00+07:00","state":"replayable","description":"-","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":{"id":"8797147","type":"video"}}}},{"id":"4391232","type":"schedule","attributes":{"title":"Gaspol (Games Asyik Paling Nampol)","start_time":"2025-07-01T07:00:00+07:00","end_time":"2025-07-01T08:29:00+07:00","state":"replayable","description":"-","image_landscape_url":"https://thumbor.prod.vidiocdn.com/UOIXZAivNrNGScU4RYzBhoG_wsc=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391232/b5f64b.jpg"},"relationships":{"video":{"data":{"id":"8797240","type":"video"}}}},{"id":"4391233","type":"schedule","attributes":{"title":"Asrama Gen Z","start_time":"2025-07-01T08:30:00+07:00","end_time":"2025-07-01T10:00:00+07:00","state":"replayable","description":"Rerun","image_landscape_url":"https://thumbor.prod.vidiocdn.com/bBOx1-eAq0GvefIFPV527rLi1PI=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/tv_program/thumbnail/387307/cd8554.jpg"},"relationships":{"video":{"data":{"id":"8796325","type":"video"}}}},{"id":"4391234","type":"schedule","attributes":{"title":"FTV Pagi: Habis Stalking Terbitlah Overthinking","start_time":"2025-07-01T10:00:00+07:00","end_time":"2025-07-01T12:00:00+07:00","state":"replayable","description":"Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik. tayang setiap hari","image_landscape_url":"https://thumbor.prod.vidiocdn.com/0Ytmu02niDTMbgoHa4W4PLbRwj8=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391234/6606b3.jpg"},"relationships":{"video":{"data":{"id":"8797548","type":"video"}}}},{"id":"4391235","type":"schedule","attributes":{"title":"LIPUTAN 6 SIANG - LIVE","start_time":"2025-07-01T12:00:00+07:00","end_time":"2025-07-01T12:29:00+07:00","state":"replayable","description":"-","image_landscape_url":"https://thumbor.prod.vidiocdn.com/IGI8qDFSLfdIxthSscvzj-ea41s=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/tv_program/thumbnail/382359/6232e3.jpg"},"relationships":{"video":{"data":{"id":"8797558","type":"video"}}}},{"id":"4391289","type":"schedule","attributes":{"title":"FTV Siang: Mau Keren Malah Jualan Es Legen","start_time":"2025-07-01T12:30:00+07:00","end_time":"2025-07-01T14:30:00+07:00","state":"replayable","description":"","image_landscape_url":"https://thumbor.prod.vidiocdn.com/WXZZP9_MrhkXEFGWvM2YWoOaak8=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391289/45e493.jpg"},"relationships":{"video":{"data":{"id":"8797657","type":"video"}}}},{"id":"4391237","type":"schedule","attributes":{"title":"Vidio Original Series: Love Is A Story","start_time":"2025-07-01T14:30:00+07:00","end_time":"2025-07-01T15:30:00+07:00","state":"replayable","description":"-","image_landscape_url":"https://thumbor.prod.vidiocdn.com/4Yto-NBssFbLobO9Py7ZQ5rtC6o=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391237/ef7c49.jpg"},"relationships":{"video":{"data":{"id":"2301297","type":"video"}}}},{"id":"4391238","type":"schedule","attributes":{"title":"Ceylan (Kuma)","start_time":"2025-07-01T15:30:00+07:00","end_time":"2025-07-01T16:20:00+07:00","state":"replayable","description":"Ceylan lari dari rumah dan bertemu dengan Karan, pengusaha muda kaya raya. Mereka jatuh cinta dan akan menikah. Namun ketika Ceylan dijebak atas pembunuhan, Karan justru menikahi janda kakaknya dan menjadikan Ceylan Kuma-nya, istri kedua yang akan melahirkan anak-anaknya. Drama Turki , tayang Senin sampai minggu, mulai pukul 14.30 WIB","image_landscape_url":"https://thumbor.prod.vidiocdn.com/pEvD0ZiZ9plFuFnPjCqQCJttmjQ=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391238/4f6afe.jpg"},"relationships":{"video":{"data":{"id":"8570604","type":"video"}}}},{"id":"4391239","type":"schedule","attributes":{"title":"Asmara Gen Z","start_time":"2025-07-01T17:00:00+07:00","end_time":"2025-07-01T18:30:00+07:00","state":"replayable","description":"Sinetron yang dibintangi oleh PARA GEN-Z. Dengan cerita dan suasana yang identik dengan GEN-Z. Tayang Setiap Hari Pukul 18145 WIB.","image_landscape_url":"https://thumbor.prod.vidiocdn.com/bBOx1-eAq0GvefIFPV527rLi1PI=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/tv_program/thumbnail/387307/cd8554.jpg"},"relationships":{"video":{"data":{"id":"8797997","type":"video"}}}},{"id":"4391240","type":"schedule","attributes":{"title":"Ketika Cinta Memanggilmu","start_time":"2025-07-01T18:45:00+07:00","end_time":"2025-07-01T20:15:00+07:00","state":"replayable","description":"Sinetron yang diperankan oleh NATASHA WILONA, RIONALDO STOCKHORST, CAKRAWALA AIRAWAN dan sederet artis lainnya. Tayang setiap hari pukul 18.20 WIB.","image_landscape_url":"https://thumbor.prod.vidiocdn.com/JoQU6k-b20XuNFbooYtjrHwp8Es=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391240/b1f053.jpg"},"relationships":{"video":{"data":{"id":"8798127","type":"video"}}}},{"id":"4391416","type":"schedule","attributes":{"title":"Seharum Cinta Melati","start_time":"2025-07-01T20:15:00+07:00","end_time":"2025-07-01T21:35:00+07:00","state":"replayable","description":"","image_landscape_url":"https://thumbor.prod.vidiocdn.com/s4HZ8vFvFAcb070PDlyoo9zvs8M=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/livestreaming/schedule/thumbnail/4391416/c9256c.jpg"},"relationships":{"video":{"data":{"id":"8798306","type":"video"}}}},{"id":"4391241","type":"schedule","attributes":{"title":"Luka Cinta","start_time":"2025-07-01T21:35:00+07:00","end_time":"2025-07-01T23:00:00+07:00","state":"replayable","description":"Sinetron drama keluarga yang diperankan oleh DINDA KIRANA, JEROME KURNIA, BILLY DAVIDSON dan bintang-bintang lainnya. Tayang setiap hari.","image_landscape_url":"https://thumbor.prod.vidiocdn.com/276KbfMxyYFORUJBbOAv4E_RNGo=/287x162/filters:strip_icc():quality(70)/vidio-media-production/uploads/tv_program/thumbnail/386197/8b082c.jpg"},"relationships":{"video":{"data":{"id":"8798228","type":"video"}}}},{"id":"4391242","type":"schedule","attributes":{"title":"Ftv Primetime Bolak Balik Belok Mondar Mandir Pantang Mundur","start_time":"2025-07-01T23:00:00+07:00","end_time":"2025-07-02T00:29:00+07:00","state":"live","description":"Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik. tayang setiap hari","image_landscape_url":"https://thumbor.prod.vidiocdn.com/X1Jifsdsbcup7OiK9hkskCy_pbs=/287x162/filters:strip_icc():quality(70)/vidio-web-prod-livestreaming/uploads/livestreaming/image/204/sctv-f23abc.jpg"},"relationships":{"video":{"data":null}}}],"included":[{"id":"8796735","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796735-ftv-primetime-cinta-dodol-inilah-yang-membuatku-lengket-padamu-30-juni-2025","embed":"https://www.vidio.com/embed/8796735?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796735?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796735/up_next?page%5Bnumber%5D=20"}},{"id":"8796793","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796793-ftv-utama-rembulan-di-kandang-sapi-01-juli-2025","embed":"https://www.vidio.com/embed/8796793?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796793?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796793/up_next?page%5Bnumber%5D=20"}},{"id":"8796823","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796823-solusi-01-juli-2025","embed":"https://www.vidio.com/embed/8796823?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796823?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796823/up_next?page%5Bnumber%5D=20"}},{"id":"8796853","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796853-buser-01-juli-2025","embed":"https://www.vidio.com/embed/8796853?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796853?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796853/up_next?page%5Bnumber%5D=20"}},{"id":"8796878","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796878-sinema-dini-hari-babysitter-macho-01-juli-2025","embed":"https://www.vidio.com/embed/8796878?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796878?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796878/up_next?page%5Bnumber%5D=20"}},{"id":"8796943","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796943-sinetron-dini-hari-cinta-karena-cinta-01-juli-2025","embed":"https://www.vidio.com/embed/8796943?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796943?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796943/up_next?page%5Bnumber%5D=20"}},{"id":"8796957","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796957-barakallah-01-juli-2025","embed":"https://www.vidio.com/embed/8796957?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796957?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796957/up_next?page%5Bnumber%5D=20"}},{"id":"8797080","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797080-liputan-6-pagi-live-01-juli-2025","embed":"https://www.vidio.com/embed/8797080?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797080?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797080/up_next?page%5Bnumber%5D=20"}},{"id":"8797147","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797147-hot-shot-01-juli-2025","embed":"https://www.vidio.com/embed/8797147?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797147?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797147/up_next?page%5Bnumber%5D=20"}},{"id":"8797240","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797240-gaspol-games-asyik-paling-nampol-01-juli-2025","embed":"https://www.vidio.com/embed/8797240?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797240?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797240/up_next?page%5Bnumber%5D=20"}},{"id":"8796325","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8796325-episode-213-part-1-2","embed":"https://www.vidio.com/embed/8796325?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8796325?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8796325/up_next?page%5Bnumber%5D=20"}},{"id":"8797548","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797548-ftv-pagi-habis-stalking-terbitlah-overthinking-01-juli-2025","embed":"https://www.vidio.com/embed/8797548?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797548?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797548/up_next?page%5Bnumber%5D=20"}},{"id":"8797558","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797558-liputan-6-siang-live-01-juli-2025","embed":"https://www.vidio.com/embed/8797558?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797558?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797558/up_next?page%5Bnumber%5D=20"}},{"id":"8797657","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797657-ftv-siang-mau-keren-malah-jualan-es-legen-01-juli-2025","embed":"https://www.vidio.com/embed/8797657?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797657?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797657/up_next?page%5Bnumber%5D=20"}},{"id":"2301297","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/2301297-ep-01-salam-kenal-komite","embed":"https://www.vidio.com/embed/2301297?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/2301297?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/2301297/up_next?page%5Bnumber%5D=20"}},{"id":"8570604","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8570604-episode-01","embed":"https://www.vidio.com/embed/8570604?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8570604?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8570604/up_next?page%5Bnumber%5D=20"}},{"id":"8797997","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8797997-episode-214-part-1-2","embed":"https://www.vidio.com/embed/8797997?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8797997?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8797997/up_next?page%5Bnumber%5D=20"}},{"id":"8798127","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8798127-episode-170-part-1-2","embed":"https://www.vidio.com/embed/8798127?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8798127?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8798127/up_next?page%5Bnumber%5D=20"}},{"id":"8798306","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8798306-episode-1-dan-2-part-1-2","embed":"https://www.vidio.com/embed/8798306?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8798306?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8798306/up_next?page%5Bnumber%5D=20"}},{"id":"8798228","type":"video","attributes":{},"relationships":{},"links":{"watchpage":"https://www.vidio.com/watch/8798228-episode-307-part-1-2","embed":"https://www.vidio.com/embed/8798228?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true","embed_preview":"https://www.vidio.com/embed/8798228?autoplay=true\u0026live_chat=false\u0026mute=true\u0026player_only=true\u0026preview_unpublished=true","up_next":"https://api.vidio.com/videos/8798228/up_next?page%5Bnumber%5D=20"}}],"meta":{"schedules_group":[{"date":"2025-06-30","schedule_ids":["4389878"]},{"date":"2025-07-01","schedule_ids":["4391225","4391226","4391227","4391228","4391324","4391229","4391230","4391231","4391232","4391233","4391234","4391235","4391289","4391237","4391238","4391239","4391240","4391416","4391241","4391242"]}]}} \ No newline at end of file diff --git a/sites/vidio.com/__data__/no_content.json b/sites/vidio.com/__data__/no_content.json new file mode 100644 index 00000000..f3c83650 --- /dev/null +++ b/sites/vidio.com/__data__/no_content.json @@ -0,0 +1 @@ +{"errors":[{"detail":"Livestreaming not found"}]} \ No newline at end of file diff --git a/sites/vidio.com/vidio.com.channels.xml b/sites/vidio.com/vidio.com.channels.xml index bc6ef7c3..54e56fde 100644 --- a/sites/vidio.com/vidio.com.channels.xml +++ b/sites/vidio.com/vidio.com.channels.xml @@ -2,12 +2,14 @@ ABC Australia AFRICANEWS TV + Ajwa TV Aljazeera ANTV Arirang Bein 1 Bein 2 Bein 3 + BeritaSatu BTV CTV 1 CTV 2 @@ -15,36 +17,44 @@ CTV 5 CTV 6 Premier League TV + Champions Golf 1 + Champions Golf 2 News Asia DAAI TV + Daystar TV DW English Elshinta TV Euro News + GGS TV + Hip Hip Horee! + Horee Indosiar Jaktv jawaposTV JTV Kompas TV Magna TV - Metro Globe Network + Makkah TV + MDTV Metro TV Moji + MUSICA NBA TV - NET TV NHK World Japan Nusantara TV RTV - RANS Channel ROCK Entertainment Rock Action SCTV - SEA TODAY SPOTV 2 SPOTV + Tawaf TV Trans7 TRANS TV TV5Monde TVN TVOne - TVRI + TVRI + U-Channel TV + Zoomoo diff --git a/sites/vidio.com/vidio.com.config.js b/sites/vidio.com/vidio.com.config.js index 8f740a78..3d0ab272 100644 --- a/sites/vidio.com/vidio.com.config.js +++ b/sites/vidio.com/vidio.com.config.js @@ -1,112 +1,89 @@ -const cheerio = require('cheerio') +const axios = require('axios') const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') const customParseFormat = require('dayjs/plugin/customParseFormat') +const crypto = require('crypto') dayjs.extend(utc) dayjs.extend(timezone) dayjs.extend(customParseFormat) -const tz = 'Asia/Jakarta' +const WEB_CLIENT_SECRET = Buffer.from('dPr0QImQ7bc5o9LMntNba2DOsSbZcjUh') +const WEB_CLIENT_IV = Buffer.from('C8RWsrtFsoeyCyPt') module.exports = { site: 'vidio.com', days: 2, - url({ channel }) { - return `https://www.vidio.com/live/${channel.site_id}/schedules` + url({ date, channel }) { + return `https://api.vidio.com/livestreamings/${channel.site_id}/schedules?filter[date]=${date.format('YYYY-MM-DD')}` }, - parser({ content, date }) { + request: { + async headers() { + const session = await loadSessionDetails() + if (!session || !session.api_key) return null + + var cipher = crypto.createCipheriv('aes-256-cbc', WEB_CLIENT_SECRET, WEB_CLIENT_IV) + return { + 'X-API-Key': cipher.update(session.api_key, 'utf8', 'base64') + cipher.final('base64'), + 'X-Secure-Level': 2 + } + } + }, + parser({ content }) { const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev && start < prev.start) { - start = start.add(1, 'd') - date = date.add(1, 'd') + const json = JSON.parse(content) + if (Array.isArray(json?.data)) { + for (const program of json.data) { + programs.push({ + title: program.attributes.title, + description: program.attributes.description, + start: dayjs(program.attributes.start_time), + stop: dayjs(program.attributes.end_time), + image: program.attributes.image_landscape_url + }) } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) + } return programs }, async channels() { - const axios = require('axios') - const cheerio = require('cheerio') - const result = await axios - .get('https://www.vidio.com/categories/daftar-channel-tv-radio-live-sports') + const channels = [] + const json = await axios + .get( + 'https://api.vidio.com/livestreamings?stream_type=tv_stream', + { + headers: await this.request.headers() + } + ) .then(response => response.data) .catch(console.error) - const $ = cheerio.load(result) - const itemGroups = $('div[data-variation="circle_horizontal"] ul').toArray() - const channels = [] - - itemGroups.forEach(group => { - const $group = $(group) - let skip = false - const sites = [] - const items = $group.find('a[data-testid="circle-card"]').toArray() - items.forEach(item => { - const name = $(item).find('span[data-testid="circle-title"]').text() - // skip radio channels - if (name.toLowerCase().indexOf('fm') >= 0 || name.toLowerCase().indexOf('radio') >= 0) { - skip = true - return true - } - let url = $(item).attr('href') - url = url.substr(url.lastIndexOf('/') + 1) - const matches = url.match(/(\d+)/) - sites.push({ + if (Array.isArray(json?.data)) { + for (const channel of json.data) { + channels.push({ lang: 'id', - site_id: matches[0], - name: name + site_id: channel.id, + name: channel.attributes.title }) - }) - if (!skip && sites.length) { - channels.push(...sites) } - }) + } return channels } } -function parseStart($item, date) { - const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() - const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${start}`, 'YYYY-MM-DD HH:mm', tz) -} - -function parseStop($item, date) { - const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() - const [, stop] = timeString.match(/- (\d{2}:\d{2}) WIB/) || [null, null] - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${stop}`, 'YYYY-MM-DD HH:mm', tz) -} - -function parseTitle($item) { - return $item('div.b-livestreaming-daily-schedule__item-content-title').text() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return $( - `#schedule-content-${date.format( - 'YYYYMMDD' - )} > .b-livestreaming-daily-schedule__scroll-container .b-livestreaming-daily-schedule__item` - ).toArray() -} +function loadSessionDetails() { + return axios + .post( + 'https://www.vidio.com/auth', + {}, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .then(r => r.data) + .catch(console.log) +} \ No newline at end of file diff --git a/sites/vidio.com/vidio.com.test.js b/sites/vidio.com/vidio.com.test.js index a34d262d..2e6689eb 100644 --- a/sites/vidio.com/vidio.com.test.js +++ b/sites/vidio.com/vidio.com.test.js @@ -1,53 +1,67 @@ -const { parser, url } = require('./vidio.com.config.js') +const { parser, url, request } = require('./vidio.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') 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('2021-11-24', 'YYYY-MM-DD').startOf('d') +jest.mock('axios') + +const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') const channel = { - site_id: '7464', - xmltv_id: 'AjwaTV.id' + site_id: '204', + xmltv_id: 'SCTV.id' } it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.vidio.com/live/7464/schedules') + expect(url({ channel, date })).toBe( + 'https://api.vidio.com/livestreamings/204/schedules?filter[date]=2025-07-01' + ) +}) + +it('can generate valid request headers', async () => { + axios.post.mockImplementation(url => { + if (url === 'https://www.vidio.com/auth') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/auth.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const result = await request.headers() + expect(result).toMatchObject({ + 'X-API-Key': + 'CH1ZFsN4N/MIfAds1DL9mP151CNqIpWHqZGRr+LkvUyiq3FRPuP1Kt6aK+pG3nEC1FXt0ZAAJ5FKP8QU8CZ5/jQdSYLVeFwl9NoIkegVpR6b7W2ZwbaF00OPr6ON1/FpLQ3RiUzTPpAqe7f+fwhOr0KrKy8PpCa54OHogaEjI3w=', + 'X-Secure-Level': 2, + }) }) it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel, date }).map(p => { p.start = p.start.toJSON() p.stop = p.stop.toJSON() return p }) - expect(result).toMatchObject([ - { - start: '2021-11-23T17:30:00.000Z', - stop: '2021-11-23T18:30:00.000Z', - title: '30 Hari 30 Juz' - }, - { - start: '2021-11-23T18:30:00.000Z', - stop: '2021-11-23T21:00:00.000Z', - title: 'Makkah Live' - }, - { - start: '2021-11-24T15:30:00.000Z', - stop: '2021-11-24T17:30:00.000Z', - title: 'FTV Islami' - } - ]) + expect(results.length).toBe(21) + expect(results[0]).toMatchObject({ + start: '2025-06-30T15:57:00.000Z', + stop: '2025-06-30T17:29:00.000Z', + title: 'Ftv PrimeTime : Cinta Dodol Inilah Yang Membuatku Lengket Padamu', + description: 'Film televisi yang mengangkat kisah romantisme kehidupan dengan konflik yang menarik. tayang setiap hari' + }) }) it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) }) diff --git a/tests/__data__/expected/epg_grab/guide_2.xml b/tests/__data__/expected/epg_grab/base.guide.xml similarity index 73% rename from tests/__data__/expected/epg_grab/guide_2.xml rename to tests/__data__/expected/epg_grab/base.guide.xml index ff7119fd..09471f36 100644 --- a/tests/__data__/expected/epg_grab/guide_2.xml +++ b/tests/__data__/expected/epg_grab/base.guide.xml @@ -1,8 +1,9 @@ +Channel 2https://example.com36 +Channel 1https://example.com Channel 1https://example.com -Channel 2https://example.com -Program1 (example.com) Programme1 (example.com) -Program1 (example.com) +Program1 (example.com) Programme1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/guide_3.xml b/tests/__data__/expected/epg_grab/custom_channels.guide.xml similarity index 54% rename from tests/__data__/expected/epg_grab/guide_3.xml rename to tests/__data__/expected/epg_grab/custom_channels.guide.xml index a57b3322..e2c7bf4c 100644 --- a/tests/__data__/expected/epg_grab/guide_3.xml +++ b/tests/__data__/expected/epg_grab/custom_channels.guide.xml @@ -1,14 +1,15 @@ Custom Channel 1https://example.com -Channel 1https://example2.com Custom Channel 2https://example.com -Channel 3https://example2.com -Channel 4https://example2.com -Program1 (example.com) +Channel 1https://example.com +Channel 3https://example2.com +Channel 4https://example2.com +Channel 1https://example2.com Programme1 (example.com) +Program1 (example.com) Programme1 (example2.com) -Program1 (example.com) Programme1 (example.com) -Program1 (example2.com) -Program1 (example2.com) +Program1 (example.com) +Program1 (example2.com) +Program1 (example2.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/guide.xml.gz b/tests/__data__/expected/epg_grab/guide.xml.gz deleted file mode 100644 index fb9654e9..00000000 Binary files a/tests/__data__/expected/epg_grab/guide.xml.gz and /dev/null differ diff --git a/tests/__data__/expected/epg_grab/guides/en/example.com.xml b/tests/__data__/expected/epg_grab/guides/en/example.com.xml index f3550b8c..c1270824 100644 --- a/tests/__data__/expected/epg_grab/guides/en/example.com.xml +++ b/tests/__data__/expected/epg_grab/guides/en/example.com.xml @@ -1,6 +1,6 @@ +Channel 2https://example.com36 Channel 1https://example.com -Channel 2https://example.com -Program1 (example.com) -Program1 (example.com) +Program1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/guide_4.xml b/tests/__data__/expected/epg_grab/lang.guide.xml similarity index 86% rename from tests/__data__/expected/epg_grab/guide_4.xml rename to tests/__data__/expected/epg_grab/lang.guide.xml index 5cc5fbaa..dc4b8238 100644 --- a/tests/__data__/expected/epg_grab/guide_4.xml +++ b/tests/__data__/expected/epg_grab/lang.guide.xml @@ -3,6 +3,6 @@ Channel 3https://example.com Programme1 (example.com) Programme1 (example.com) -Program1 (example.com) -Program1 (example.com) +Program1 (example.com) +Program1 (example.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/proxy.guide.xml b/tests/__data__/expected/epg_grab/proxy.guide.xml new file mode 100644 index 00000000..09471f36 --- /dev/null +++ b/tests/__data__/expected/epg_grab/proxy.guide.xml @@ -0,0 +1,9 @@ + +Channel 2https://example.com36 +Channel 1https://example.com +Channel 1https://example.com +Programme1 (example.com) +Program1 (example.com) +Programme1 (example.com) +Program1 (example.com) + \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/guide.xml b/tests/__data__/expected/epg_grab/template.guide.xml similarity index 66% rename from tests/__data__/expected/epg_grab/guide.xml rename to tests/__data__/expected/epg_grab/template.guide.xml index 66da48ee..cd54510c 100644 --- a/tests/__data__/expected/epg_grab/guide.xml +++ b/tests/__data__/expected/epg_grab/template.guide.xml @@ -1,14 +1,15 @@ +Channel 2https://example.com36 +Channel 1https://example.com Channel 1https://example.com -Channel 1https://example2.com -Channel 2https://example.com Channel 3https://example2.com -Channel 4https://example2.com -Program1 (example.com) +Channel 4https://example2.com +Channel 1https://example2.com Programme1 (example.com) +Program1 (example.com) Programme1 (example2.com) -Program1 (example.com) Programme1 (example.com) -Program1 (example2.com) -Program1 (example2.com) +Program1 (example.com) +Program1 (example2.com) +Program1 (example2.com) \ No newline at end of file diff --git a/tests/__data__/expected/epg_grab/template.guide.xml.gz b/tests/__data__/expected/epg_grab/template.guide.xml.gz new file mode 100644 index 00000000..126b426d Binary files /dev/null and b/tests/__data__/expected/epg_grab/template.guide.xml.gz differ diff --git a/tests/__data__/input/__data__/channels.json b/tests/__data__/input/__data__/channels.json index 90f9cc9d..d838427f 100644 --- a/tests/__data__/input/__data__/channels.json +++ b/tests/__data__/input/__data__/channels.json @@ -9,8 +9,7 @@ "categories": [], "is_nsfw": false, "closed": "2020-01-01", - "replaced_by": "R6.co", - "logo": "https://www.directv.com/images/logos/channels/dark/large/579.png" + "replaced_by": "R6.co" }, { "id": "Bravos.us", @@ -20,8 +19,7 @@ "subdivision": null, "city": null, "categories": [], - "is_nsfw": false, - "logo": "https://www.directv.com/images/logos/channels/dark/large/579.png" + "is_nsfw": false }, { "id": "CNNInternational.us", @@ -34,8 +32,7 @@ "categories": [ "news" ], - "is_nsfw": false, - "logo": "https://i.imgur.com/2BXCg0x.jpg" + "is_nsfw": false }, { "id": "MNetMovies2.za", @@ -45,11 +42,10 @@ "subdivision": null, "city": null, "categories": [], - "is_nsfw": false, - "logo": "https://rndcdn.dstv.com/dstvcms/2020/08/31/M-Net_Movies_2_Logo_4-3_lightbackground_xlrg.png" + "is_nsfw": false }, - {"id":"6eren.dk","name":"6'eren","alt_names":[],"network":null,"owners":["Warner Bros. Discovery EMEA"],"country":"DK","subdivision":null,"city":null,"broadcast_area":["c/DK"],"languages":["dan"],"categories":[],"is_nsfw":false,"launched":"2009-01-01","closed":null,"replaced_by":null,"website":"http://www.6-eren.dk/","logo":"https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"}, - {"id":"BBCNews.uk","name":"BBC News","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/UK"],"languages":["eng"],"categories":["news"],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"http://news.bbc.co.uk/","logo":"https://i.imgur.com/rPzH88J.png"}, + {"id":"6eren.dk","name":"6'eren","alt_names":[],"network":null,"owners":["Warner Bros. Discovery EMEA"],"country":"DK","subdivision":null,"city":null,"broadcast_area":["c/DK"],"languages":["dan"],"categories":[],"is_nsfw":false,"launched":"2009-01-01","closed":null,"replaced_by":null,"website":"http://www.6-eren.dk/"}, + {"id":"BBCNews.uk","name":"BBC News","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/UK"],"languages":["eng"],"categories":["news"],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"http://news.bbc.co.uk/"}, { "id": "CNN.us", "name": "CNN", @@ -58,9 +54,8 @@ "subdivision": null, "city": null, "categories": [], - "is_nsfw": false, - "logo": "https://www.directv.com/images/logos/channels/dark/large/579.png" + "is_nsfw": false }, - {"id":"Channel2.us","name":"Channel 2 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"","logo":"https://i.imgur.com/rPzH88J.png"}, - {"id":"Channel3.us","name":"Channel 3 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"","logo":"https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"} + {"id":"Channel2.us","name":"Channel 2 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""}, + {"id":"Channel3.us","name":"Channel 3 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""} ] \ No newline at end of file diff --git a/tests/__data__/input/__data__/feeds.json b/tests/__data__/input/__data__/feeds.json index 2aacf6b7..875f7723 100644 --- a/tests/__data__/input/__data__/feeds.json +++ b/tests/__data__/input/__data__/feeds.json @@ -26,5 +26,33 @@ ], "languages": [], "video_format": "480i" + }, + { + "channel": "Bravo.us", + "id": "East", + "name": "East", + "is_main": true, + "broadcast_area": [ + "r/EUR" + ], + "timezones": [ + "America/New_York" + ], + "languages": [], + "video_format": "480i" + }, + { + "channel": "Channel4.us", + "id": "HD", + "name": "HD", + "is_main": true, + "broadcast_area": [ + "r/EUR" + ], + "timezones": [ + "America/New_York" + ], + "languages": [], + "video_format": "480i" } ] \ No newline at end of file diff --git a/tests/__data__/input/__data__/logos.json b/tests/__data__/input/__data__/logos.json new file mode 100644 index 00000000..4aa0a9a9 --- /dev/null +++ b/tests/__data__/input/__data__/logos.json @@ -0,0 +1,4 @@ +[ + {"channel":"Channel3.us","feed":null,"tags":[],"width":334,"height":210,"format":"PNG","url":"https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"}, + {"channel":"Channel4.us","feed":"HD","tags":[],"width":334,"height":210,"format":"PNG","url":"https://i.imgur.com/BPzH88J.png"} +] \ No newline at end of file diff --git a/tests/__data__/input/channels_lint/valid.channels.xml b/tests/__data__/input/channels_lint/valid.channels.xml index 42a447bb..8a221693 100644 --- a/tests/__data__/input/channels_lint/valid.channels.xml +++ b/tests/__data__/input/channels_lint/valid.channels.xml @@ -1,4 +1,4 @@ - Bravo's + Bravo's \ No newline at end of file diff --git a/tests/__data__/input/channels_validate/wrong_xmltv_id.channels.xml b/tests/__data__/input/channels_validate/wrong_channel_id.channels.xml similarity index 52% rename from tests/__data__/input/channels_validate/wrong_xmltv_id.channels.xml rename to tests/__data__/input/channels_validate/wrong_channel_id.channels.xml index 2534dc6c..cdf398e8 100644 --- a/tests/__data__/input/channels_validate/wrong_xmltv_id.channels.xml +++ b/tests/__data__/input/channels_validate/wrong_channel_id.channels.xml @@ -1,6 +1,6 @@ CNN International - Bravo - Bravo + Bravo + Bravo \ No newline at end of file diff --git a/tests/__data__/input/channels_validate/wrong_feed_id.channels.xml b/tests/__data__/input/channels_validate/wrong_feed_id.channels.xml new file mode 100644 index 00000000..2951eabd --- /dev/null +++ b/tests/__data__/input/channels_validate/wrong_feed_id.channels.xml @@ -0,0 +1,4 @@ + + + Bravo + \ No newline at end of file diff --git a/tests/__data__/input/epg_grab/custom.channels.xml b/tests/__data__/input/epg_grab/custom.channels.xml index 9b78a7ae..e107685e 100644 --- a/tests/__data__/input/epg_grab/custom.channels.xml +++ b/tests/__data__/input/epg_grab/custom.channels.xml @@ -3,7 +3,7 @@ Custom Channel 1 Custom Channel 2 Channel 1 - Channel 3 - Channel 4 + Channel 3 + Channel 4 Channel 1 \ No newline at end of file diff --git a/tests/__data__/input/epg_grab/example.com/example.com.config.js b/tests/__data__/input/epg_grab/example.com/example.com.config.js index 7f65c30c..52370045 100644 --- a/tests/__data__/input/epg_grab/example.com/example.com.config.js +++ b/tests/__data__/input/epg_grab/example.com/example.com.config.js @@ -20,7 +20,7 @@ module.exports = { return [ { title: 'Program1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` } ] diff --git a/tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml b/tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml index 1b037f02..cfd7a7bb 100644 --- a/tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml +++ b/tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml @@ -1,6 +1,6 @@ + Channel 2 Channel 1 - Channel 2 Channel 1 \ No newline at end of file diff --git a/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js b/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js index 7f65c30c..52370045 100644 --- a/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js +++ b/tests/__data__/input/epg_grab/sites/example.com/example.com.config.js @@ -20,7 +20,7 @@ module.exports = { return [ { title: 'Program1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` } ] diff --git a/tests/__data__/input/epg_grab/sites/example2.com/example2.com.channels.xml b/tests/__data__/input/epg_grab/sites/example2.com/example2.com.channels.xml index 821bd29c..612a6a49 100644 --- a/tests/__data__/input/epg_grab/sites/example2.com/example2.com.channels.xml +++ b/tests/__data__/input/epg_grab/sites/example2.com/example2.com.channels.xml @@ -1,6 +1,6 @@ Channel 3 - Channel 4 + Channel 4 Channel 1 \ No newline at end of file diff --git a/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js b/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js index 8789ccdc..9e4f58d3 100644 --- a/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js +++ b/tests/__data__/input/epg_grab/sites/example2.com/example2.com.config.js @@ -15,7 +15,7 @@ module.exports = { return [ { title: 'Program1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`, stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` } ] diff --git a/tests/commands/channels/validate.test.ts b/tests/commands/channels/validate.test.ts index 928148dd..4dd18b37 100644 --- a/tests/commands/channels/validate.test.ts +++ b/tests/commands/channels/validate.test.ts @@ -28,20 +28,40 @@ describe('channels:validate', () => { } }) - it('will show a message if the file contains a channel with wrong xmltv_id', () => { + it('will show a message if the file contains a channel with wrong channel id', () => { try { - const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_xmltv_id.channels.xml` + const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_channel_id.channels.xml` const stdout = execSync(cmd, { encoding: 'utf8' }) if (process.env.DEBUG === 'true') console.log(cmd, stdout) process.exit(1) } catch (error) { expect((error as ExecError).status).toBe(1) expect((error as ExecError).stdout).toContain(` -┌─────────┬──────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼──────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ -│ 0 │ 'wrong_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ -└─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ +┌─────────┬────────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼────────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ +│ 0 │ 'wrong_channel_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ +└─────────┴────────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ + +1 error(s) in 1 file(s) +`) + } + }) + + it('will show a message if the file contains a channel with wrong feed id', () => { + try { + const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_feed_id.channels.xml` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain(` +┌─────────┬─────────────────┬──────┬─────────────────┬─────────┬─────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼─────────────────┼──────┼─────────────────┼─────────┼─────────┤ +│ 0 │ 'wrong_feed_id' │ 'en' │ 'Bravo.us@West' │ '150' │ 'Bravo' │ +└─────────┴─────────────────┴──────┴─────────────────┴─────────┴─────────┘ 1 error(s) in 1 file(s) `) diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 2f7f05d3..3459f99a 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -4,7 +4,8 @@ import { Zip } from '@freearhey/core' import fs from 'fs-extra' import path from 'path' -const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/input/epg_grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/__data__' +const ENV_VAR = + 'cross-env SITES_DIR=tests/__data__/input/epg_grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/__data__' beforeEach(() => { fs.emptyDirSync('tests/__data__/output') @@ -14,85 +15,12 @@ describe('epg:grab', () => { it('can grab epg by site name', () => { const cmd = `${ENV_VAR} npm run grab --- --site=example.com --output="${path.resolve( 'tests/__data__/output/guide.xml' - )}"` + )}" --timeout=100` const stdout = execSync(cmd, { encoding: 'utf8' }) if (process.env.DEBUG === 'true') console.log(cmd, stdout) expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide_2.xml') - ) - }) - - it('can grab epg with multiple channels.xml files', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide.xml') - ) - }) - - it('can grab epg with gzip option enabled', async () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output="${path.resolve( - 'tests/__data__/output/guide.xml' - )}" --gzip` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide.xml') - ) - - const zip = new Zip() - const expected = zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) - const result = zip.decompress( - fs.readFileSync('tests/__data__/expected/epg_grab/guide.xml.gz') - ) - expect(expected).toEqual(result) - }, 30000) - - it('can grab epg with wildcard as output', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels="tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml" --output="tests/__data__/output/guides/{lang}/{site}.xml"` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guides/en/example.com.xml') - ) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') - ) - }) - - it('can grab epg then language filter enabled', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') - ) - }) - - it('can grab epg then using a multi-language filter', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{site}.xml --lang=fr,it` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guides/example.com.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide_4.xml') - ) - }) - - it('can grab epg using custom channels list', () => { - const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml` - const stdout = execSync(cmd, { encoding: 'utf8' }) - if (process.env.DEBUG === 'true') console.log(cmd, stdout) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide_3.xml') + content('tests/__data__/expected/epg_grab/base.guide.xml') ) }) @@ -119,29 +47,112 @@ describe('epg:grab', () => { expect(errorThrown).toBe(true) }) + it('can grab epg with wildcard as output', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels="tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml" --output="tests/__data__/output/guides/{lang}/{site}.xml" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/guides/en/example.com.xml') + ) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') + ) + }) + + it('can grab epg then language filter enabled', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml') + ) + }) + + it('can grab epg then using a multi-language filter', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{site}.xml --lang=fr,it --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guides/example.com.xml')).toEqual( + content('tests/__data__/expected/epg_grab/lang.guide.xml') + ) + }) + it('can grab epg via https proxy', () => { const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=https://bob:123456@proxy.com:1234 --output="${path.resolve( 'tests/__data__/output/guide.xml' - )}"` + )}" --timeout=100` const stdout = execSync(cmd, { encoding: 'utf8' }) if (process.env.DEBUG === 'true') console.log(cmd, stdout) expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide_2.xml') + content('tests/__data__/expected/epg_grab/proxy.guide.xml') ) }) it('can grab epg via socks5 proxy', () => { const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=socks5://bob:123456@proxy.com:1234 --output="${path.resolve( 'tests/__data__/output/guide.xml' - )}"` + )}" --timeout=100` const stdout = execSync(cmd, { encoding: 'utf8' }) if (process.env.DEBUG === 'true') console.log(cmd, stdout) expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/epg_grab/guide_2.xml') + content('tests/__data__/expected/epg_grab/proxy.guide.xml') ) }) + + it('can grab epg with curl option', () => { + const cmd = `${ENV_VAR} npm run grab --- --site=example.com --curl --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(stdout).toContain('curl https://example.com') + }) + + it('can grab epg with multiple channels.xml files', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/template.guide.xml') + ) + }) + + it('can grab epg using custom channels list', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/custom_channels.guide.xml') + ) + }) + + it('can grab epg with gzip option enabled', () => { + const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output="${path.resolve( + 'tests/__data__/output/guide.xml' + )}" --gzip --timeout=100` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/epg_grab/template.guide.xml') + ) + + const zip = new Zip() + const expected = zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) + const result = zip.decompress( + fs.readFileSync('tests/__data__/expected/epg_grab/template.guide.xml.gz') + ) + expect(expected).toEqual(result) + }) }) function content(filepath: string) {