mirror of
https://github.com/iptv-org/epg.git
synced 2025-05-11 01:20:08 -04:00
commit
950f18a30a
52 changed files with 3735 additions and 1247 deletions
File diff suppressed because it is too large
Load diff
|
@ -10,10 +10,39 @@ document.addEventListener('alpine:init', () => {
|
|||
},
|
||||
|
||||
async init() {
|
||||
this.items = await fetch('items.json')
|
||||
const countries = await fetch('api/countries.json')
|
||||
.then(response => response.json())
|
||||
.catch(console.log)
|
||||
|
||||
const channels = await fetch('api/channels.json')
|
||||
.then(response => response.json())
|
||||
.catch(console.log)
|
||||
|
||||
let items = {}
|
||||
for (let channel of channels) {
|
||||
if (!items[channel.country]) {
|
||||
const country = countries[channel.country]
|
||||
|
||||
items[channel.country] = {
|
||||
flag: country.flag,
|
||||
name: country.name,
|
||||
expanded: false,
|
||||
channels: []
|
||||
}
|
||||
}
|
||||
|
||||
channel.hash = `${channel.id}_${channel.name}`.toLowerCase()
|
||||
|
||||
items[channel.country].channels.push(channel)
|
||||
}
|
||||
|
||||
items = Object.values(items).sort((a, b) => {
|
||||
if (a.name > b.name) return 1
|
||||
if (a.name < b.name) return -1
|
||||
return 0
|
||||
})
|
||||
|
||||
this.items = items
|
||||
this.isLoading = false
|
||||
}
|
||||
}))
|
||||
|
|
|
@ -60,18 +60,19 @@
|
|||
<div class="level-item">Loading...</div>
|
||||
</div>
|
||||
|
||||
<template x-for="country in items">
|
||||
<template x-for="item in items">
|
||||
<div
|
||||
class="card mb-3 is-shadowless"
|
||||
style="border: 1px solid #dbdbdb"
|
||||
x-data="{
|
||||
count: 0,
|
||||
get countryChannels() {
|
||||
if (!_query) return country.channels
|
||||
if (!_query) return item.channels
|
||||
const normQuery = _query.toLowerCase()
|
||||
|
||||
return country.channels.filter(c => {
|
||||
return c.hash.includes(_query)
|
||||
})
|
||||
return item.channels.filter(c => {
|
||||
return c.hash.includes(normQuery)
|
||||
}) || []
|
||||
}
|
||||
}"
|
||||
x-show="countryChannels.length > 0"
|
||||
|
@ -81,14 +82,14 @@
|
|||
>
|
||||
<div
|
||||
class="card-header is-shadowless is-clickable"
|
||||
@click="country.expanded = !country.expanded"
|
||||
@click="item.expanded = !item.expanded"
|
||||
>
|
||||
<span class="card-header-title" x-text="`${country.flag} ${country.name}`"></span>
|
||||
<span class="card-header-title" x-text="`${item.flag} ${item.name}`"></span>
|
||||
<button class="card-header-icon" aria-label="more options">
|
||||
<span class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512">
|
||||
<path
|
||||
x-show="!country.expanded"
|
||||
x-show="!item.expanded"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
|
@ -97,7 +98,7 @@
|
|||
d="M112 184l144 144 144-144"
|
||||
/>
|
||||
<path
|
||||
x-show="country.expanded"
|
||||
x-show="item.expanded"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
|
@ -109,7 +110,7 @@
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content" x-show="country.expanded || (count > 0 && _query.length)">
|
||||
<div class="card-content" x-show="item.expanded || (count > 0 && _query.length)">
|
||||
<div class="table-container">
|
||||
<table class="table" style="min-width: 100%">
|
||||
<thead>
|
||||
|
@ -132,10 +133,12 @@
|
|||
/>
|
||||
</td>
|
||||
<td class="is-vcentered" nowrap>
|
||||
<div x-text="channel.display_name"></div>
|
||||
<template x-for="name in channel.name">
|
||||
<p x-text="name"></p>
|
||||
</template>
|
||||
</td>
|
||||
<td class="is-vcentered" nowrap>
|
||||
<code x-text="channel.tvg_id"></code>
|
||||
<code x-text="channel.id"></code>
|
||||
</td>
|
||||
<td class="is-vcentered">
|
||||
<template x-for="guide in channel.guides">
|
||||
|
|
265
.github/workflows/auto-update.yml
vendored
265
.github/workflows/auto-update.yml
vendored
|
@ -2,211 +2,126 @@ name: auto-update
|
|||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 0,12 * * *'
|
||||
jobs:
|
||||
delete-old-branch:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- name: Delete Branch
|
||||
uses: dawidd6/action-delete-branch@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branches: 'bot/auto-update'
|
||||
create-branch:
|
||||
runs-on: ubuntu-latest
|
||||
needs: delete-old-branch
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- name: Create Branch
|
||||
uses: peterjgrainger/action-create-branch@v2.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
branch: 'bot/auto-update'
|
||||
create-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-branch
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: bot/auto-update
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Create Matrix
|
||||
id: set-matrix
|
||||
run: node scripts/create-matrix.js
|
||||
grab:
|
||||
- run: npm install
|
||||
- run: node scripts/commands/create-database.js
|
||||
- run: node scripts/commands/create-matrix.js
|
||||
id: create-matrix
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: database
|
||||
path: scripts/database
|
||||
outputs:
|
||||
matrix: ${{ steps.create-matrix.outputs.matrix }}
|
||||
load:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-matrix
|
||||
needs: setup
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{fromJSON(needs.create-matrix.outputs.matrix)}}
|
||||
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
|
||||
# matrix:
|
||||
# cluster_id: [1]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
ref: bot/auto-update
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
name: database
|
||||
path: scripts/database
|
||||
- uses: FedericoCarboni/setup-ffmpeg@v1
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Run Grabber
|
||||
run: NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/${{ matrix.guide.site }}/${{ matrix.guide.site }}.config.js --channels=sites/${{ matrix.guide.site }}/${{ matrix.guide.site }}_${{ matrix.guide.country }}.channels.xml --output=.gh-pages/guides/${{ matrix.guide.country }}/${{ matrix.guide.site }}.epg.xml --log=logs/${{ matrix.guide.site }}_${{ matrix.guide.country }}.log --days=2 --timeout=30000
|
||||
- name: Upload Guide
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: .gh-pages
|
||||
path: .gh-pages/
|
||||
if-no-files-found: error
|
||||
- name: Upload Log
|
||||
uses: actions/upload-artifact@v2
|
||||
- run: npm install
|
||||
- run: NODE_OPTIONS=--insecure-http-parser node scripts/commands/load-cluster.js --days=2 --timeout=30000 --cluster-id=${{ matrix.cluster_id }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: logs
|
||||
path: logs/
|
||||
if-no-files-found: error
|
||||
deploy:
|
||||
needs: grab
|
||||
path: scripts/logs
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
needs: load
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- run: echo "::set-output name=branch_name::$(date +'bot/auto-update-%s')"
|
||||
id: create-branch-name
|
||||
- run: git config user.name 'iptv-bot[bot]'
|
||||
- run: git config user.email '84861620+iptv-bot[bot]@users.noreply.github.com'
|
||||
- run: git checkout -b ${{ steps.create-branch-name.outputs.branch_name }}
|
||||
- run: curl -L -o scripts/data/codes.json https://iptv-org.github.io/epg/codes.json
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
ref: bot/auto-update
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
- name: Generate Token
|
||||
uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
name: database
|
||||
path: scripts/database
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: .gh-pages/guides
|
||||
target-folder: guides
|
||||
clean: false
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
git-config-name: iptv-bot
|
||||
git-config-email: 84861620+iptv-bot[bot]@users.noreply.github.com
|
||||
commit-message: '[Bot] Deploy to GitHub Pages'
|
||||
update-codes:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: bot/auto-update
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
name: logs
|
||||
path: scripts/logs
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Update Codes
|
||||
run: npm run update-codes
|
||||
- name: Generate Token
|
||||
uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
- run: npm install
|
||||
- run: node scripts/commands/save-results.js
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: database
|
||||
path: scripts/database
|
||||
- run: node scripts/commands/update-api.js
|
||||
- run: NODE_OPTIONS="--max-old-space-size=4096" node scripts/commands/update-guides.js
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: logs
|
||||
path: scripts/logs
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: gh-pages
|
||||
path: .gh-pages
|
||||
- run: node scripts/commands/update-readme.js
|
||||
- run: git add README.md
|
||||
- run: git commit -m "[Bot] Update README.md"
|
||||
- run: git push -u origin ${{ steps.create-branch-name.outputs.branch_name }}
|
||||
- uses: tibdex/github-app-token@v1
|
||||
if: ${{ !env.ACT }}
|
||||
id: create-app-token
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
- uses: repo-sync/pull-request@v2
|
||||
if: ${{ !env.ACT }}
|
||||
id: pull-request
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: .gh-pages
|
||||
clean: false
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
git-config-name: iptv-bot
|
||||
git-config-email: 84861620+iptv-bot[bot]@users.noreply.github.com
|
||||
commit-message: '[Bot] Deploy to GitHub Pages'
|
||||
update-readme:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: bot/auto-update
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'npm'
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Update README.md
|
||||
run: npx ts-node scripts/update-readme.ts
|
||||
- name: Commit Changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: '[Bot] Update README.md'
|
||||
commit_user_name: iptv-bot
|
||||
commit_user_email: 84861620+iptv-bot[bot]@users.noreply.github.com
|
||||
commit_author: 'iptv-bot[bot] <84861620+iptv-bot[bot]@users.noreply.github.com>'
|
||||
branch: bot/auto-update
|
||||
file_pattern: README.md
|
||||
pull-request:
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
needs: update-readme
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: bot/auto-update
|
||||
- name: Generate Token
|
||||
uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- name: Create Pull Request
|
||||
id: pr
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: 'bot/auto-update'
|
||||
github_token: ${{ steps.create-app-token.outputs.token }}
|
||||
source_branch: ${{ steps.create-branch-name.outputs.branch_name }}
|
||||
destination_branch: 'master'
|
||||
pr_title: '[Bot] Update guides'
|
||||
pr_title: '[Bot] Daily update'
|
||||
pr_body: |
|
||||
This pull request is created by [auto-update][1] workflow.
|
||||
|
||||
[1]: https://github.com/iptv-org/epg/actions/runs/${{ github.run_id }}
|
||||
github_token: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Merge Pull Request
|
||||
if: ${{ steps.pr.outputs.pr_number != 0 }}
|
||||
uses: juliangruber/merge-pull-request-action@v1
|
||||
[1]: https://github.com/iptv-org/iptv/actions/runs/${{ github.run_id }}
|
||||
- uses: juliangruber/merge-pull-request-action@v1
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
with:
|
||||
github-token: ${{ secrets.PAT }}
|
||||
number: ${{ steps.pr.outputs.pr_number }}
|
||||
number: ${{ steps.pull-request.outputs.pr_number }}
|
||||
method: squash
|
||||
- uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: .gh-pages
|
||||
token: ${{ steps.create-app-token.outputs.token }}
|
||||
git-config-name: iptv-bot[bot]
|
||||
git-config-email: 84861620+iptv-bot[bot]@users.noreply.github.com
|
||||
commit-message: '[Bot] Deploy to GitHub Pages'
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
logs/
|
||||
guide.xml
|
||||
guide.xml
|
||||
.artifacts
|
|
@ -30,7 +30,7 @@ https://iptv-org.github.io/epg/index.html
|
|||
You can also get a list of all available channels and their codes in JSON format by sending a GET request to:
|
||||
|
||||
```
|
||||
https://iptv-org.github.io/epg/codes.json
|
||||
https://iptv-org.github.io/epg/api/channels.json
|
||||
```
|
||||
|
||||
If successful, you should get the following response:
|
||||
|
@ -43,10 +43,12 @@ If successful, you should get the following response:
|
|||
[
|
||||
...
|
||||
{
|
||||
"tvg_id": "CNNUSA.us",
|
||||
"display_name": "CNN USA",
|
||||
"id": "CNNUSA.us",
|
||||
"name": [
|
||||
"CNN USA"
|
||||
],
|
||||
"logo": "https://cdn.tvpassport.com/image/station/100x100/cnn.png",
|
||||
"country": "us",
|
||||
"country": "US",
|
||||
"guides": [
|
||||
"https://iptv-org.github.io/epg/guides/tvtv.us.guide.xml",
|
||||
...
|
||||
|
|
293
package-lock.json
generated
293
package-lock.json
generated
|
@ -12,7 +12,7 @@
|
|||
"commander": "^8.2.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"epg-grabber": "^0.14.0",
|
||||
"epg-grabber": "^0.15.2",
|
||||
"epg-parser": "^0.1.6",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^7.2.0",
|
||||
|
@ -21,13 +21,14 @@
|
|||
"lodash": "^4.17.21",
|
||||
"markdown-include": "^0.4.3",
|
||||
"mockdate": "^3.0.5",
|
||||
"mz": "^2.7.0",
|
||||
"nedb-promises": "^5.0.3",
|
||||
"parse-duration": "^1.0.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"srcset": "^4.0.0",
|
||||
"tabletojson": "^2.0.7",
|
||||
"ts-node": "^10.3.0",
|
||||
"typescript": "^4.4.4",
|
||||
"wildcard-match": "^5.1.2"
|
||||
"wildcard-match": "^5.1.2",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
@ -573,6 +574,8 @@
|
|||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
|
@ -581,6 +584,8 @@
|
|||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
},
|
||||
|
@ -850,6 +855,26 @@
|
|||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@seald-io/binary-search-tree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.2.tgz",
|
||||
"integrity": "sha512-+pYGvPFAk7wUR+ONMOlc6A+LUN4kOCFwyPLjyaeS7wVibADPHWYJNYsNtyIAwjF1AXQkuaXElnIc4XjKt55QZA=="
|
||||
},
|
||||
"node_modules/@seald-io/nedb": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-2.2.0.tgz",
|
||||
"integrity": "sha512-whkcx3hpcowNhoSEbIsrfe8TXxDwyj8SJJut2EqF7DSX2GGqQlL7Ix/vzwwOo4FJolzDhZD2DaUTVKmTQS3Rog==",
|
||||
"dependencies": {
|
||||
"@seald-io/binary-search-tree": "^1.0.2",
|
||||
"async": "0.2.10",
|
||||
"localforage": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@seald-io/nedb/node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
|
||||
|
@ -899,22 +924,30 @@
|
|||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.1.16",
|
||||
|
@ -1139,6 +1172,11 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
|
@ -1154,7 +1192,9 @@
|
|||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
|
@ -1652,7 +1692,9 @@
|
|||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
|
@ -1838,6 +1880,8 @@
|
|||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
|
@ -1963,9 +2007,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/epg-grabber": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.14.0.tgz",
|
||||
"integrity": "sha512-klrReY5Rqy0DS/2zy1XKYzj5nDbzArLjB3GktmrXueuFePt3poPuRVk7/Sav6sKvLB3bV1rtwrGZdETk1Avaaw==",
|
||||
"version": "0.15.2",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.15.2.tgz",
|
||||
"integrity": "sha512-waXwyUkp+O9Wmos95H2DUb5KyFAgm6GrFUcsWY19G7vme4qx9paH2DhKlw7pPT/aIe5sTJm/jSNG25xTK1K8Fw==",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
|
@ -2439,6 +2483,11 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz",
|
||||
|
@ -3395,6 +3444,22 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
@ -3464,7 +3529,9 @@
|
|||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
|
@ -3563,11 +3630,29 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
|
||||
},
|
||||
"node_modules/nedb-promises": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-5.0.3.tgz",
|
||||
"integrity": "sha512-hqQZi/T3dMJXQraFohVdatNX1uPDTGuVrqMiEnwA0dJxlC90xctOFG+jeUzPy9hYQGqCV+CpkSBkvUvTJdUApQ==",
|
||||
"dependencies": {
|
||||
"@seald-io/nedb": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||
|
@ -3637,6 +3722,14 @@
|
|||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
||||
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ=="
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
@ -4312,6 +4405,25 @@
|
|||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify-all": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/throat": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
|
||||
|
@ -4374,6 +4486,8 @@
|
|||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz",
|
||||
"integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
|
@ -4414,6 +4528,8 @@
|
|||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
@ -4465,6 +4581,8 @@
|
|||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
|
||||
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -4763,6 +4881,8 @@
|
|||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -5169,12 +5289,16 @@
|
|||
"@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
}
|
||||
|
@ -5386,6 +5510,28 @@
|
|||
"chalk": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@seald-io/binary-search-tree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.2.tgz",
|
||||
"integrity": "sha512-+pYGvPFAk7wUR+ONMOlc6A+LUN4kOCFwyPLjyaeS7wVibADPHWYJNYsNtyIAwjF1AXQkuaXElnIc4XjKt55QZA=="
|
||||
},
|
||||
"@seald-io/nedb": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-2.2.0.tgz",
|
||||
"integrity": "sha512-whkcx3hpcowNhoSEbIsrfe8TXxDwyj8SJJut2EqF7DSX2GGqQlL7Ix/vzwwOo4FJolzDhZD2DaUTVKmTQS3Rog==",
|
||||
"requires": {
|
||||
"@seald-io/binary-search-tree": "^1.0.2",
|
||||
"async": "0.2.10",
|
||||
"localforage": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
|
||||
|
@ -5423,22 +5569,30 @@
|
|||
"@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.1.16",
|
||||
|
@ -5632,6 +5786,11 @@
|
|||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
|
@ -5644,7 +5803,9 @@
|
|||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
|
@ -6044,7 +6205,9 @@
|
|||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
|
@ -6177,7 +6340,9 @@
|
|||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"diff-sequences": {
|
||||
"version": "27.0.6",
|
||||
|
@ -6266,9 +6431,9 @@
|
|||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
|
||||
},
|
||||
"epg-grabber": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.14.0.tgz",
|
||||
"integrity": "sha512-klrReY5Rqy0DS/2zy1XKYzj5nDbzArLjB3GktmrXueuFePt3poPuRVk7/Sav6sKvLB3bV1rtwrGZdETk1Avaaw==",
|
||||
"version": "0.15.2",
|
||||
"resolved": "https://registry.npmjs.org/epg-grabber/-/epg-grabber-0.15.2.tgz",
|
||||
"integrity": "sha512-waXwyUkp+O9Wmos95H2DUb5KyFAgm6GrFUcsWY19G7vme4qx9paH2DhKlw7pPT/aIe5sTJm/jSNG25xTK1K8Fw==",
|
||||
"requires": {
|
||||
"axios": "^0.21.1",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
|
@ -6600,6 +6765,11 @@
|
|||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||
},
|
||||
"import-local": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz",
|
||||
|
@ -7314,6 +7484,22 @@
|
|||
"type-check": "~0.3.2"
|
||||
}
|
||||
},
|
||||
"lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
|
||||
"requires": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"requires": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
@ -7368,7 +7554,9 @@
|
|||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"makeerror": {
|
||||
"version": "1.0.12",
|
||||
|
@ -7446,11 +7634,29 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
|
||||
},
|
||||
"nedb-promises": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-5.0.3.tgz",
|
||||
"integrity": "sha512-hqQZi/T3dMJXQraFohVdatNX1uPDTGuVrqMiEnwA0dJxlC90xctOFG+jeUzPy9hYQGqCV+CpkSBkvUvTJdUApQ==",
|
||||
"requires": {
|
||||
"@seald-io/nedb": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node-ensure": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||
|
@ -7502,6 +7708,11 @@
|
|||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
|
||||
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ=="
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
@ -8000,6 +8211,22 @@
|
|||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||
},
|
||||
"thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"thenify-all": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
|
||||
"requires": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
}
|
||||
},
|
||||
"throat": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
|
||||
|
@ -8050,6 +8277,8 @@
|
|||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz",
|
||||
"integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
|
@ -8068,7 +8297,9 @@
|
|||
"acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8106,7 +8337,9 @@
|
|||
"typescript": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
|
||||
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA=="
|
||||
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.2",
|
||||
|
@ -8336,7 +8569,9 @@
|
|||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
package.json
14
package.json
|
@ -2,13 +2,14 @@
|
|||
"name": "epg",
|
||||
"scripts": {
|
||||
"update-codes": "node scripts/update-codes.js",
|
||||
"test": "jest"
|
||||
"act": "act workflow_dispatch -W .github/workflows/auto-update.yml --artifact-server-path ./.artifacts",
|
||||
"test": "npx jest --runInBand"
|
||||
},
|
||||
"private": true,
|
||||
"author": "Arhey",
|
||||
"license": "MIT",
|
||||
"jest": {
|
||||
"testRegex": "sites/(.*?/)?.*test.js$"
|
||||
"testRegex": "(sites|tests)/(.*?/)?.*test.js$"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
|
@ -16,7 +17,7 @@
|
|||
"commander": "^8.2.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"epg-grabber": "^0.14.0",
|
||||
"epg-grabber": "^0.15.2",
|
||||
"epg-parser": "^0.1.6",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^7.2.0",
|
||||
|
@ -25,12 +26,13 @@
|
|||
"lodash": "^4.17.21",
|
||||
"markdown-include": "^0.4.3",
|
||||
"mockdate": "^3.0.5",
|
||||
"mz": "^2.7.0",
|
||||
"nedb-promises": "^5.0.3",
|
||||
"parse-duration": "^1.0.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"srcset": "^4.0.0",
|
||||
"tabletojson": "^2.0.7",
|
||||
"ts-node": "^10.3.0",
|
||||
"typescript": "^4.4.4",
|
||||
"wildcard-match": "^5.1.2"
|
||||
"wildcard-match": "^5.1.2",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
||||
|
|
3
scripts/.gitignore
vendored
3
scripts/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
edit.js
|
||||
database/
|
||||
logs/
|
|
@ -1,46 +0,0 @@
|
|||
const { Command } = require('commander')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { json2xml } = require('./utils')
|
||||
|
||||
const program = new Command()
|
||||
program
|
||||
.requiredOption('-c, --config <config>', 'Config file')
|
||||
.option('-s, --set [args...]', 'Set custom arguments', [])
|
||||
.option('-o, --output <output>', 'Output file')
|
||||
.parse(process.argv)
|
||||
|
||||
const options = program.opts()
|
||||
|
||||
async function main() {
|
||||
const config = require(path.resolve(options.config))
|
||||
const args = {}
|
||||
options.set.forEach(arg => {
|
||||
const [key, value] = arg.split(':')
|
||||
args[key] = value
|
||||
})
|
||||
let channels = config.channels(args)
|
||||
if (isPromise(channels)) {
|
||||
channels = await channels
|
||||
}
|
||||
channels = channels.map(channel => {
|
||||
if (!channel.xmltv_id) {
|
||||
channel.xmltv_id = channel.name
|
||||
}
|
||||
return channel
|
||||
})
|
||||
const xml = json2xml(channels, config.site)
|
||||
|
||||
const dir = path.parse(options.config).dir
|
||||
const output = options.output || `${dir}/${config.site}.channels.xml`
|
||||
|
||||
fs.writeFileSync(path.resolve(output), xml)
|
||||
|
||||
console.log(`File '${output}' successfully saved`)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
function isPromise(promise) {
|
||||
return !!promise && typeof promise.then === 'function'
|
||||
}
|
|
@ -1,3 +1,49 @@
|
|||
const { Command } = require('commander')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const program = new Command()
|
||||
program
|
||||
.requiredOption('-c, --config <config>', 'Config file')
|
||||
.option('-s, --set [args...]', 'Set custom arguments', [])
|
||||
.option('-o, --output <output>', 'Output file')
|
||||
.parse(process.argv)
|
||||
|
||||
const options = program.opts()
|
||||
|
||||
async function main() {
|
||||
const config = require(path.resolve(options.config))
|
||||
const args = {}
|
||||
options.set.forEach(arg => {
|
||||
const [key, value] = arg.split(':')
|
||||
args[key] = value
|
||||
})
|
||||
let channels = config.channels(args)
|
||||
if (isPromise(channels)) {
|
||||
channels = await channels
|
||||
}
|
||||
channels = channels.map(channel => {
|
||||
if (!channel.xmltv_id) {
|
||||
channel.xmltv_id = channel.name
|
||||
}
|
||||
return channel
|
||||
})
|
||||
const xml = json2xml(channels, config.site)
|
||||
|
||||
const dir = path.parse(options.config).dir
|
||||
const output = options.output || `${dir}/${config.site}.channels.xml`
|
||||
|
||||
fs.writeFileSync(path.resolve(output), xml)
|
||||
|
||||
console.log(`File '${output}' successfully saved`)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
function isPromise(promise) {
|
||||
return !!promise && typeof promise.then === 'function'
|
||||
}
|
||||
|
||||
function json2xml(items, site) {
|
||||
let output = `<?xml version="1.0" encoding="UTF-8"?>\r\n<site site="${site}">\r\n <channels>\r\n`
|
||||
|
70
scripts/commands/create-database.js
Normal file
70
scripts/commands/create-database.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
const { db, file, parser, logger } = require('../core')
|
||||
const { program } = require('commander')
|
||||
const _ = require('lodash')
|
||||
|
||||
const options = program
|
||||
.option(
|
||||
'--max-clusters <max-clusters>',
|
||||
'Set maximum number of clusters',
|
||||
parser.parseNumber,
|
||||
256
|
||||
)
|
||||
.option('--channels <channels>', 'Set path to channels.xml file', 'sites/**/*.channels.xml')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
const channels = []
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting...')
|
||||
logger.info(`Number of clusters: ${options.maxClusters}`)
|
||||
|
||||
await loadChannels()
|
||||
await saveToDatabase()
|
||||
|
||||
logger.info('Done')
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function loadChannels() {
|
||||
logger.info(`Loading channels...`)
|
||||
|
||||
const files = await file.list(options.channels)
|
||||
for (const filepath of files) {
|
||||
const dir = file.dirname(filepath)
|
||||
const filename = file.basename(filepath)
|
||||
const [_, gid] = filename.match(/_([a-z-]+)\.channels\.xml/i) || [null, null]
|
||||
const items = await parser.parseChannels(filepath)
|
||||
for (const item of items) {
|
||||
const countryCode = item.xmltv_id.split('.')[1]
|
||||
item.country = countryCode ? countryCode.toUpperCase() : null
|
||||
item.channelsPath = filepath
|
||||
item.configPath = `${dir}/${item.site}.config.js`
|
||||
item.gid = gid
|
||||
channels.push(item)
|
||||
}
|
||||
}
|
||||
logger.info(`Found ${channels.length} channels`)
|
||||
}
|
||||
|
||||
async function saveToDatabase() {
|
||||
logger.info('Saving to the database...')
|
||||
await db.channels.load()
|
||||
await db.channels.reset()
|
||||
const chunks = split(_.shuffle(channels), options.maxClusters)
|
||||
for (const [i, chunk] of chunks.entries()) {
|
||||
for (const item of chunk) {
|
||||
item.cluster_id = i + 1
|
||||
await db.channels.insert(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function split(arr, n) {
|
||||
let result = []
|
||||
for (let i = n; i > 0; i--) {
|
||||
result.push(arr.splice(0, Math.ceil(arr.length / i)))
|
||||
}
|
||||
return result
|
||||
}
|
16
scripts/commands/create-matrix.js
Normal file
16
scripts/commands/create-matrix.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const { logger, db } = require('../core')
|
||||
|
||||
async function main() {
|
||||
await db.channels.load()
|
||||
const docs = await db.channels.find({}).sort({ cluster_id: 1 })
|
||||
const cluster_id = docs.reduce((acc, curr) => {
|
||||
if (!acc.includes(curr.cluster_id)) acc.push(curr.cluster_id)
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const matrix = { cluster_id }
|
||||
const output = `::set-output name=matrix::${JSON.stringify(matrix)}`
|
||||
logger.info(output)
|
||||
}
|
||||
|
||||
main()
|
80
scripts/commands/load-cluster.js
Normal file
80
scripts/commands/load-cluster.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
const _ = require('lodash')
|
||||
const grabber = require('epg-grabber')
|
||||
const { program } = require('commander')
|
||||
const { db, logger, timer, file, parser } = require('../core')
|
||||
|
||||
const options = program
|
||||
.requiredOption('-c, --cluster-id <cluster-id>', 'The ID of cluster to load', parser.parseNumber)
|
||||
.option('--days <days>', 'Number of days for which to grab the program', parser.parseNumber, 1)
|
||||
.option('--delay <delay>', 'Delay between requests (in mileseconds)', parser.parseNumber)
|
||||
.option(
|
||||
'-t, --timeout <timeout>',
|
||||
'Set a timeout for each request (in mileseconds)',
|
||||
parser.parseNumber
|
||||
)
|
||||
.option('--debug', 'Enable debug mode', false)
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/logs'
|
||||
|
||||
async function main() {
|
||||
logger.info('Starting...')
|
||||
timer.start()
|
||||
|
||||
const clusterLog = `${LOGS_DIR}/load-cluster/cluster_${options.clusterId}.log`
|
||||
logger.info(`Loading cluster: ${options.clusterId}`)
|
||||
logger.info(`Creating '${clusterLog}'...`)
|
||||
await file.create(clusterLog)
|
||||
await db.channels.load()
|
||||
const channels = await db.channels.find({ cluster_id: options.clusterId })
|
||||
const total = options.days * channels.length
|
||||
logger.info(`Total ${total} requests`)
|
||||
|
||||
logger.info('Loading...')
|
||||
const results = {}
|
||||
let i = 1
|
||||
for (const channel of channels) {
|
||||
let config = require(file.resolve(channel.configPath))
|
||||
|
||||
config = _.merge(config, {
|
||||
days: options.days,
|
||||
debug: options.debug,
|
||||
delay: options.delay,
|
||||
request: {
|
||||
timeout: options.timeout
|
||||
}
|
||||
})
|
||||
|
||||
const programs = await grabber.grab(channel, config, async (data, err) => {
|
||||
await db.channels.update({ _id: channel._id }, { $set: { logo: data.channel.logo } })
|
||||
|
||||
logger.info(
|
||||
`[${i}/${total}] ${channel.site} - ${data.id} - ${data.date.format('MMM D, YYYY')} (${
|
||||
data.programs.length
|
||||
} programs)`
|
||||
)
|
||||
|
||||
if (err) logger.error(err.message)
|
||||
|
||||
if (i < total) i++
|
||||
})
|
||||
|
||||
await file.append(
|
||||
clusterLog,
|
||||
JSON.stringify({
|
||||
_id: channel._id,
|
||||
site: channel.site,
|
||||
country: channel.country,
|
||||
gid: channel.gid,
|
||||
programs
|
||||
}) + '\n'
|
||||
)
|
||||
}
|
||||
|
||||
db.channels.compact()
|
||||
|
||||
logger.info(`Done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
||||
}
|
||||
|
||||
main()
|
27
scripts/commands/save-results.js
Normal file
27
scripts/commands/save-results.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const { db, logger, file, parser } = require('../core')
|
||||
const _ = require('lodash')
|
||||
|
||||
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/logs'
|
||||
|
||||
async function main() {
|
||||
await db.programs.load()
|
||||
await db.programs.reset()
|
||||
const files = await file.list(`${LOGS_DIR}/load-cluster/cluster_*.log`)
|
||||
for (const filepath of files) {
|
||||
logger.info(`Parsing "${filepath}"...`)
|
||||
const results = await parser.parseLogs(filepath)
|
||||
for (const result of results) {
|
||||
const programs = result.programs.map(program => {
|
||||
program.site = result.site
|
||||
program.country = result.country
|
||||
program.gid = result.gid
|
||||
|
||||
return program
|
||||
})
|
||||
|
||||
await db.programs.insert(programs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
54
scripts/commands/update-api.js
Normal file
54
scripts/commands/update-api.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
const { db, logger, file, xml } = require('../core')
|
||||
const _ = require('lodash')
|
||||
|
||||
const DB_DIR = process.env.DB_DIR || 'scripts/database'
|
||||
const API_DIR = process.env.API_DIR || '.gh-pages/api'
|
||||
|
||||
async function main() {
|
||||
await generateChannelsJson()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateChannelsJson() {
|
||||
logger.info('Generating channels.json...')
|
||||
|
||||
const channels = await loadChannels()
|
||||
|
||||
const channelsPath = `${API_DIR}/channels.json`
|
||||
logger.info(`Saving to "${channelsPath}"...`)
|
||||
await file.create(channelsPath, JSON.stringify(channels))
|
||||
|
||||
logger.info(`Done`)
|
||||
}
|
||||
|
||||
async function loadChannels() {
|
||||
logger.info('Loading channels from database...')
|
||||
|
||||
await db.channels.load()
|
||||
|
||||
const items = await db.channels.find({}).sort({ xmltv_id: 1 })
|
||||
|
||||
const output = {}
|
||||
for (const item of items) {
|
||||
if (!output[item.xmltv_id]) {
|
||||
output[item.xmltv_id] = {
|
||||
id: item.xmltv_id,
|
||||
name: [],
|
||||
logo: item.logo || null,
|
||||
country: item.country,
|
||||
guides: []
|
||||
}
|
||||
} else {
|
||||
output[item.xmltv_id].logo = output[item.xmltv_id].logo || item.logo
|
||||
}
|
||||
|
||||
output[item.xmltv_id].name.push(item.name)
|
||||
output[item.xmltv_id].name = _.uniq(output[item.xmltv_id].name)
|
||||
output[item.xmltv_id].guides.push(
|
||||
`https://iptv-org.github.io/epg/guides/${item.gid}/${item.site}.epg.xml`
|
||||
)
|
||||
}
|
||||
|
||||
return Object.values(output)
|
||||
}
|
95
scripts/commands/update-guides.js
Normal file
95
scripts/commands/update-guides.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
const { db, logger, file, xml } = require('../core')
|
||||
const _ = require('lodash')
|
||||
|
||||
const DB_DIR = process.env.DB_DIR || 'scripts/database'
|
||||
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/logs'
|
||||
const PUBLIC_DIR = process.env.PUBLIC_DIR || '.gh-pages'
|
||||
const LOG_PATH = `${LOGS_DIR}/update-guides.log`
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
await generateGuides()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateGuides() {
|
||||
logger.info(`Generating guides/...`)
|
||||
|
||||
const channels = await loadChannels()
|
||||
const programs = await loadPrograms()
|
||||
|
||||
const grouped = _.groupBy(programs, i => `${i.gid}_${i.site}`)
|
||||
for (let key in grouped) {
|
||||
const [gid, site] = key.split('_') || [null, null]
|
||||
const filepath = `${PUBLIC_DIR}/guides/${gid}/${site}.epg.xml`
|
||||
const groupProgs = grouped[key]
|
||||
const groupChannels = Object.keys(_.groupBy(groupProgs, i => `${i.site}_${i.channel}`)).map(
|
||||
key => {
|
||||
let [site, channel] = key.split('_')
|
||||
|
||||
return channels.find(i => i.xmltv_id === channel && i.site === site)
|
||||
}
|
||||
)
|
||||
|
||||
const output = xml.create({ channels: groupChannels, programs: groupProgs })
|
||||
|
||||
logger.info(`Creating "${filepath}"...`)
|
||||
await file.create(filepath, output)
|
||||
|
||||
await log({
|
||||
gid,
|
||||
site,
|
||||
count: groupChannels.length,
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Done`)
|
||||
}
|
||||
|
||||
async function loadChannels() {
|
||||
logger.info('Loading channels...')
|
||||
|
||||
await db.channels.load()
|
||||
|
||||
return await db.channels.find({}).sort({ xmltv_id: 1 })
|
||||
}
|
||||
|
||||
async function loadPrograms() {
|
||||
logger.info('Loading programs...')
|
||||
|
||||
logger.info('Loading "database/programs.db"...')
|
||||
await db.programs.load()
|
||||
|
||||
logger.info('Loading programs from "database/programs.db"...')
|
||||
let programs = await db.programs.find({}).sort({ channel: 1, start: 1 })
|
||||
|
||||
programs = programs.map(program => {
|
||||
return {
|
||||
title: program.title ? [{ lang: program.lang, value: program.title }] : [],
|
||||
description: program.description ? [{ lang: program.lang, value: program.description }] : [],
|
||||
categories: program.category ? [{ lang: program.lang, value: program.category }] : [],
|
||||
icon: program.icon,
|
||||
channel: program.channel,
|
||||
lang: program.lang,
|
||||
start: program.start,
|
||||
stop: program.stop,
|
||||
site: program.site,
|
||||
country: program.country,
|
||||
gid: program.gid,
|
||||
_id: program._id
|
||||
}
|
||||
})
|
||||
|
||||
return programs
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
logger.info(`Creating '${LOG_PATH}'...`)
|
||||
await file.create(LOG_PATH)
|
||||
}
|
||||
|
||||
async function log(data) {
|
||||
await file.append(LOG_PATH, JSON.stringify(data) + '\n')
|
||||
}
|
118
scripts/commands/update-readme.js
Normal file
118
scripts/commands/update-readme.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
const { file, markdown, parser, logger } = require('../core')
|
||||
const countries = require('../data/countries.json')
|
||||
const states = require('../data/us-states.json')
|
||||
const provinces = require('../data/ca-provinces.json')
|
||||
const { program } = require('commander')
|
||||
const _ = require('lodash')
|
||||
|
||||
const LOGS_DIR = process.env.LOGS_DIR || 'scripts/logs'
|
||||
const LOG_PATH = `${LOGS_DIR}/update-guides.log`
|
||||
|
||||
let log = []
|
||||
|
||||
const options = program
|
||||
.option('-c, --config <config>', 'Set path to config file', '.readme/config.json')
|
||||
.parse(process.argv)
|
||||
.opts()
|
||||
|
||||
async function main() {
|
||||
await setUp()
|
||||
|
||||
await generateCountriesTable()
|
||||
await generateUSStatesTable()
|
||||
await generateCanadaProvincesTable()
|
||||
|
||||
await updateReadme()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
async function generateCountriesTable() {
|
||||
logger.info('Generating countries table...')
|
||||
|
||||
const items = log.filter(i => i.gid.length === 2)
|
||||
let rows = []
|
||||
for (const item of items) {
|
||||
const code = item.gid.toUpperCase()
|
||||
const country = countries[code]
|
||||
|
||||
rows.push({
|
||||
flag: country.flag,
|
||||
name: country.name,
|
||||
channels: item.count,
|
||||
epg: `<code>https://iptv-org.github.io/epg/guides/${item.gid}/${item.site}.epg.xml</code>`
|
||||
})
|
||||
}
|
||||
|
||||
rows = _.orderBy(rows, ['name', 'channels'], ['asc', 'desc'])
|
||||
rows = _.groupBy(rows, 'name')
|
||||
|
||||
const table = markdown.createTable(rows, ['Country', 'Channels', 'EPG'])
|
||||
|
||||
await file.create('./.readme/_countries.md', table)
|
||||
}
|
||||
|
||||
async function generateUSStatesTable() {
|
||||
logger.info('Generating US states table...')
|
||||
|
||||
const items = log.filter(i => i.gid.startsWith('us-'))
|
||||
let rows = []
|
||||
for (const item of items) {
|
||||
const code = item.gid.toUpperCase()
|
||||
const state = states[code]
|
||||
|
||||
rows.push({
|
||||
name: state.name,
|
||||
channels: item.count,
|
||||
epg: `<code>https://iptv-org.github.io/epg/guides/${item.gid}/${item.site}.epg.xml</code>`
|
||||
})
|
||||
}
|
||||
|
||||
rows = _.orderBy(rows, ['name', 'channels'], ['asc', 'desc'])
|
||||
rows = _.groupBy(rows, 'name')
|
||||
|
||||
const table = markdown.createTable(rows, ['State', 'Channels', 'EPG'])
|
||||
|
||||
await file.create('./.readme/_us-states.md', table)
|
||||
}
|
||||
|
||||
async function generateCanadaProvincesTable() {
|
||||
logger.info('Generating Canada provinces table...')
|
||||
|
||||
const items = log.filter(i => i.gid.startsWith('ca-'))
|
||||
let rows = []
|
||||
for (const item of items) {
|
||||
const code = item.gid.toUpperCase()
|
||||
const province = provinces[code]
|
||||
|
||||
rows.push({
|
||||
name: province.name,
|
||||
channels: item.count,
|
||||
epg: `<code>https://iptv-org.github.io/epg/guides/${item.gid}/${item.site}.epg.xml</code>`
|
||||
})
|
||||
}
|
||||
|
||||
rows = _.orderBy(rows, ['name', 'channels'], ['asc', 'desc'])
|
||||
rows = _.groupBy(rows, 'name')
|
||||
|
||||
const table = markdown.createTable(rows, ['Province', 'Channels', 'EPG'])
|
||||
|
||||
await file.create('./.readme/_ca-provinces.md', table)
|
||||
}
|
||||
|
||||
async function updateReadme() {
|
||||
logger.info('Updating README.md...')
|
||||
|
||||
const config = require(file.resolve(options.config))
|
||||
await file.createDir(file.dirname(config.build))
|
||||
await markdown.compile(options.config)
|
||||
}
|
||||
|
||||
async function setUp() {
|
||||
log = await parser.parseLogs(LOG_PATH)
|
||||
|
||||
if (!log.length) {
|
||||
logger.error(`File "${LOG_PATH}" is empty`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
76
scripts/core/db.js
Normal file
76
scripts/core/db.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
const nedb = require('nedb-promises')
|
||||
const file = require('./file')
|
||||
|
||||
const DB_DIR = process.env.DB_DIR || './scripts/database'
|
||||
|
||||
class Database {
|
||||
constructor(filepath) {
|
||||
this.filepath = filepath
|
||||
}
|
||||
|
||||
load() {
|
||||
this.db = nedb.create({
|
||||
filename: file.resolve(this.filepath),
|
||||
autoload: true,
|
||||
onload: err => {
|
||||
if (err) console.error(err)
|
||||
},
|
||||
compareStrings: (a, b) => {
|
||||
a = a.replace(/\s/g, '_')
|
||||
b = b.replace(/\s/g, '_')
|
||||
|
||||
return a.localeCompare(b, undefined, {
|
||||
sensitivity: 'accent',
|
||||
numeric: true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
removeIndex(field) {
|
||||
return this.db.removeIndex(field)
|
||||
}
|
||||
|
||||
addIndex(options) {
|
||||
return this.db.ensureIndex(options)
|
||||
}
|
||||
|
||||
compact() {
|
||||
return this.db.persistence.compactDatafile()
|
||||
}
|
||||
|
||||
stopAutocompact() {
|
||||
return this.db.persistence.stopAutocompaction()
|
||||
}
|
||||
|
||||
reset() {
|
||||
return file.clear(this.filepath)
|
||||
}
|
||||
|
||||
count(query) {
|
||||
return this.db.count(query)
|
||||
}
|
||||
|
||||
insert(doc) {
|
||||
return this.db.insert(doc)
|
||||
}
|
||||
|
||||
update(query, update) {
|
||||
return this.db.update(query, update)
|
||||
}
|
||||
|
||||
find(query) {
|
||||
return this.db.find(query)
|
||||
}
|
||||
|
||||
remove(query, options) {
|
||||
return this.db.remove(query, options)
|
||||
}
|
||||
}
|
||||
|
||||
const db = {}
|
||||
|
||||
db.channels = new Database(`${DB_DIR}/channels.db`)
|
||||
db.programs = new Database(`${DB_DIR}/programs.db`)
|
||||
|
||||
module.exports = db
|
68
scripts/core/file.js
Normal file
68
scripts/core/file.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
const path = require('path')
|
||||
const glob = require('glob')
|
||||
const fs = require('mz/fs')
|
||||
|
||||
const file = {}
|
||||
|
||||
file.list = function (pattern) {
|
||||
return new Promise(resolve => {
|
||||
glob(pattern, function (err, files) {
|
||||
resolve(files)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
file.getFilename = function (filepath) {
|
||||
return path.parse(filepath).name
|
||||
}
|
||||
|
||||
file.createDir = async function (dir) {
|
||||
if (await file.exists(dir)) return
|
||||
|
||||
return fs.mkdir(dir, { recursive: true }).catch(console.error)
|
||||
}
|
||||
|
||||
file.exists = function (filepath) {
|
||||
return fs.exists(path.resolve(filepath))
|
||||
}
|
||||
|
||||
file.read = function (filepath) {
|
||||
return fs.readFile(path.resolve(filepath), { encoding: 'utf8' }).catch(console.error)
|
||||
}
|
||||
|
||||
file.append = function (filepath, data) {
|
||||
return fs.appendFile(path.resolve(filepath), data).catch(console.error)
|
||||
}
|
||||
|
||||
file.create = function (filepath, data = '') {
|
||||
filepath = path.resolve(filepath)
|
||||
const dir = path.dirname(filepath)
|
||||
|
||||
return file
|
||||
.createDir(dir)
|
||||
.then(() => file.write(filepath, data))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
file.write = function (filepath, data = '') {
|
||||
return fs.writeFile(path.resolve(filepath), data, { encoding: 'utf8' }).catch(console.error)
|
||||
}
|
||||
|
||||
file.clear = async function (filepath) {
|
||||
if (await file.exists(filepath)) return file.write(filepath, '')
|
||||
return true
|
||||
}
|
||||
|
||||
file.resolve = function (filepath) {
|
||||
return path.resolve(filepath)
|
||||
}
|
||||
|
||||
file.dirname = function (filepath) {
|
||||
return path.dirname(filepath)
|
||||
}
|
||||
|
||||
file.basename = function (filepath) {
|
||||
return path.basename(filepath)
|
||||
}
|
||||
|
||||
module.exports = file
|
7
scripts/core/index.js
Normal file
7
scripts/core/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
exports.db = require('./db')
|
||||
exports.logger = require('./logger')
|
||||
exports.file = require('./file')
|
||||
exports.parser = require('./parser')
|
||||
exports.timer = require('./timer')
|
||||
exports.xml = require('./xml')
|
||||
exports.markdown = require('./markdown')
|
42
scripts/core/logger.js
Normal file
42
scripts/core/logger.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
const { createLogger, format, transports, addColors } = require('winston')
|
||||
const { combine, timestamp, printf } = format
|
||||
|
||||
const consoleFormat = ({ level, message, timestamp }) => {
|
||||
if (typeof message === 'object') return JSON.stringify(message)
|
||||
return message
|
||||
}
|
||||
|
||||
const config = {
|
||||
levels: {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
failed: 3,
|
||||
success: 4,
|
||||
http: 5,
|
||||
verbose: 6,
|
||||
debug: 7,
|
||||
silly: 8
|
||||
},
|
||||
colors: {
|
||||
info: 'white',
|
||||
success: 'green',
|
||||
failed: 'red'
|
||||
}
|
||||
}
|
||||
|
||||
const t = [
|
||||
new transports.Console({
|
||||
format: format.combine(format.printf(consoleFormat))
|
||||
})
|
||||
]
|
||||
|
||||
const logger = createLogger({
|
||||
transports: t,
|
||||
levels: config.levels,
|
||||
level: 'verbose'
|
||||
})
|
||||
|
||||
addColors(config.colors)
|
||||
|
||||
module.exports = logger
|
41
scripts/core/markdown.js
Normal file
41
scripts/core/markdown.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
const markdownInclude = require('markdown-include')
|
||||
const file = require('./file')
|
||||
|
||||
const markdown = {}
|
||||
|
||||
markdown.createTable = function (data, cols) {
|
||||
let output = '<table>\n'
|
||||
|
||||
output += ' <thead>\n <tr>'
|
||||
for (let column of cols) {
|
||||
output += `<th align="left">${column}</th>`
|
||||
}
|
||||
output += '</tr>\n </thead>\n'
|
||||
|
||||
output += ' <tbody>\n'
|
||||
for (let groupId in data) {
|
||||
const group = data[groupId]
|
||||
for (let [i, item] of group.entries()) {
|
||||
const rowspan = group.length > 1 ? ` rowspan="${group.length}"` : ''
|
||||
output += ' <tr>'
|
||||
if (i === 0) {
|
||||
const name = item.flag ? `${item.flag} ${item.name}` : item.name
|
||||
output += `<td align="left" valign="top" nowrap${rowspan}>${name}</td>`
|
||||
}
|
||||
output += `<td align="right">${item.channels}</td>`
|
||||
output += `<td align="left" nowrap>${item.epg}</td>`
|
||||
output += '</tr>\n'
|
||||
}
|
||||
}
|
||||
output += ' </tbody>\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
markdown.compile = function (filepath) {
|
||||
markdownInclude.compileFiles(file.resolve(filepath))
|
||||
}
|
||||
|
||||
module.exports = markdown
|
31
scripts/core/parser.js
Normal file
31
scripts/core/parser.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const logger = require('./logger')
|
||||
const file = require('./file')
|
||||
const grabber = require('epg-grabber')
|
||||
|
||||
const parser = {}
|
||||
|
||||
parser.parseChannels = async function(filepath) {
|
||||
const content = await file.read(filepath)
|
||||
const channels = grabber.parseChannels(content)
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
parser.parseLogs = async function (filepath) {
|
||||
const content = await file.read(filepath)
|
||||
if (!content) return []
|
||||
const lines = content.split('\n')
|
||||
|
||||
return lines.map(line => (line ? JSON.parse(line) : null)).filter(l => l)
|
||||
}
|
||||
|
||||
parser.parseNumber = function (string) {
|
||||
const parsed = parseInt(string)
|
||||
if (isNaN(parsed)) {
|
||||
throw new Error('scripts/core/parser.js:parseNumber() Input value is not a number')
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
module.exports = parser
|
29
scripts/core/timer.js
Normal file
29
scripts/core/timer.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const { performance } = require('perf_hooks')
|
||||
const dayjs = require('dayjs')
|
||||
const duration = require('dayjs/plugin/duration')
|
||||
const relativeTime = require('dayjs/plugin/relativeTime')
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(duration)
|
||||
|
||||
const timer = {}
|
||||
|
||||
let t0 = 0
|
||||
|
||||
timer.start = function () {
|
||||
t0 = performance.now()
|
||||
}
|
||||
|
||||
timer.format = function (f) {
|
||||
let t1 = performance.now()
|
||||
|
||||
return dayjs.duration(t1 - t0).format(f)
|
||||
}
|
||||
|
||||
timer.humanize = function (suffix = true) {
|
||||
let t1 = performance.now()
|
||||
|
||||
return dayjs.duration(t1 - t0).humanize(suffix)
|
||||
}
|
||||
|
||||
module.exports = timer
|
79
scripts/core/xml.js
Normal file
79
scripts/core/xml.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
|
||||
const xml = {}
|
||||
|
||||
xml.create = function ({ channels, programs }) {
|
||||
let output = `<?xml version="1.0" encoding="UTF-8" ?><tv>\n`
|
||||
for (let channel of channels) {
|
||||
output += `<channel id="${escapeString(channel.xmltv_id)}">`
|
||||
output += `<display-name>${escapeString(channel.name)}</display-name>`
|
||||
if (channel.logo) output += `<icon src="${escapeString(channel.logo)}"/>`
|
||||
if (channel.site) output += `<url>https://${channel.site}</url>`
|
||||
output += `</channel>\n`
|
||||
}
|
||||
|
||||
for (let program of programs) {
|
||||
if (!program) continue
|
||||
|
||||
const start = program.start ? dayjs.unix(program.start).utc().format('YYYYMMDDHHmmss ZZ') : ''
|
||||
const stop = program.stop ? dayjs.unix(program.stop).utc().format('YYYYMMDDHHmmss ZZ') : ''
|
||||
|
||||
if (start && stop) {
|
||||
output += `<programme start="${start}" stop="${stop}" channel="${escapeString(
|
||||
program.channel
|
||||
)}">`
|
||||
|
||||
program.title.forEach(title => {
|
||||
output += `<title lang="${title.lang}">${escapeString(title.value)}</title>`
|
||||
})
|
||||
|
||||
program.description.forEach(description => {
|
||||
output += `<desc lang="${description.lang}">${escapeString(description.value)}</desc>`
|
||||
})
|
||||
|
||||
program.categories.forEach(category => {
|
||||
output += `<category lang="${category.lang}">${escapeString(category.value)}</category>`
|
||||
})
|
||||
|
||||
if (program.image) output += `<icon src="${escapeString(program.image)}"/>`
|
||||
|
||||
output += '</programme>\n'
|
||||
}
|
||||
}
|
||||
|
||||
output += '</tv>'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
module.exports = xml
|
||||
|
||||
function escapeString(string, defaultValue = '') {
|
||||
if (!string) 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'
|
||||
)
|
||||
|
||||
string = String(string || '').replace(regex, '')
|
||||
|
||||
return string
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n|\r/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.trim()
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
const { Command } = require('commander')
|
||||
const file = require('./file')
|
||||
const path = require('path')
|
||||
|
||||
const program = new Command()
|
||||
program
|
||||
.option('--include <include>', 'List of files to include', parseList)
|
||||
.option('--exclude <exclude>', 'List of files to exclude', parseList)
|
||||
.parse(process.argv)
|
||||
|
||||
const options = program.opts()
|
||||
|
||||
file.list('sites/*/*.channels.xml', options.include, options.exclude).then(files => {
|
||||
const matrix = {
|
||||
guide: []
|
||||
}
|
||||
|
||||
files.forEach(filename => {
|
||||
const [_, site, country] = filename.match(/sites\/.*\/(.*)_(.*)\.channels\.xml/i)
|
||||
const config = require(path.resolve(`./sites/${site}/${site}.config.js`))
|
||||
|
||||
if (config.ignore) return
|
||||
|
||||
matrix.guide.push({ site, country })
|
||||
})
|
||||
|
||||
const output = `::set-output name=matrix::${JSON.stringify(matrix)}`
|
||||
console.log(output)
|
||||
})
|
||||
|
||||
function parseList(str) {
|
||||
if (!str) return []
|
||||
|
||||
return str.split(',')
|
||||
}
|
67
scripts/data/ca-provinces.json
Normal file
67
scripts/data/ca-provinces.json
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"CA-AB":
|
||||
{
|
||||
"name": "Alberta",
|
||||
"code": "CA-AB"
|
||||
},
|
||||
"CA-BC":
|
||||
{
|
||||
"name": "British Columbia",
|
||||
"code": "CA-BC"
|
||||
},
|
||||
"CA-MB":
|
||||
{
|
||||
"name": "Manitoba",
|
||||
"code": "CA-MB"
|
||||
},
|
||||
"CA-NB":
|
||||
{
|
||||
"name": "New Brunswick",
|
||||
"code": "CA-NB"
|
||||
},
|
||||
"CA-NL":
|
||||
{
|
||||
"name": "Newfoundland and Labrador",
|
||||
"code": "CA-NL"
|
||||
},
|
||||
"CA-NT":
|
||||
{
|
||||
"name": "Northwest Territories",
|
||||
"code": "CA-NT"
|
||||
},
|
||||
"CA-NS":
|
||||
{
|
||||
"name": "Nova Scotia",
|
||||
"code": "CA-NS"
|
||||
},
|
||||
"CA-NU":
|
||||
{
|
||||
"name": "Nunavut",
|
||||
"code": "CA-NU"
|
||||
},
|
||||
"CA-ON":
|
||||
{
|
||||
"name": "Ontario",
|
||||
"code": "CA-ON"
|
||||
},
|
||||
"CA-PE":
|
||||
{
|
||||
"name": "Prince Edward Island",
|
||||
"code": "CA-PE"
|
||||
},
|
||||
"CA-QC":
|
||||
{
|
||||
"name": "Quebec",
|
||||
"code": "CA-QC"
|
||||
},
|
||||
"CA-SK":
|
||||
{
|
||||
"name": "Saskatchewan",
|
||||
"code": "CA-SK"
|
||||
},
|
||||
"CA-YT":
|
||||
{
|
||||
"name": "Yukon Territory",
|
||||
"code": "CA-YT"
|
||||
}
|
||||
}
|
1092
scripts/data/countries.json
Normal file
1092
scripts/data/countries.json
Normal file
File diff suppressed because it is too large
Load diff
297
scripts/data/us-states.json
Normal file
297
scripts/data/us-states.json
Normal file
|
@ -0,0 +1,297 @@
|
|||
{
|
||||
"US-AL":
|
||||
{
|
||||
"name": "Alabama",
|
||||
"code": "US-AL"
|
||||
},
|
||||
"US-AK":
|
||||
{
|
||||
"name": "Alaska",
|
||||
"code": "US-AK"
|
||||
},
|
||||
"US-AS":
|
||||
{
|
||||
"name": "American Samoa",
|
||||
"code": "US-AS"
|
||||
},
|
||||
"US-AZ":
|
||||
{
|
||||
"name": "Arizona",
|
||||
"code": "US-AZ"
|
||||
},
|
||||
"US-AR":
|
||||
{
|
||||
"name": "Arkansas",
|
||||
"code": "US-AR"
|
||||
},
|
||||
"US-CA":
|
||||
{
|
||||
"name": "California",
|
||||
"code": "US-CA"
|
||||
},
|
||||
"US-CO":
|
||||
{
|
||||
"name": "Colorado",
|
||||
"code": "US-CO"
|
||||
},
|
||||
"US-CT":
|
||||
{
|
||||
"name": "Connecticut",
|
||||
"code": "US-CT"
|
||||
},
|
||||
"US-DE":
|
||||
{
|
||||
"name": "Delaware",
|
||||
"code": "US-DE"
|
||||
},
|
||||
"US-DC":
|
||||
{
|
||||
"name": "District Of Columbia",
|
||||
"code": "US-DC"
|
||||
},
|
||||
"US-FM":
|
||||
{
|
||||
"name": "Federated States Of Micronesia",
|
||||
"code": "US-FM"
|
||||
},
|
||||
"US-FL":
|
||||
{
|
||||
"name": "Florida",
|
||||
"code": "US-FL"
|
||||
},
|
||||
"US-GA":
|
||||
{
|
||||
"name": "Georgia",
|
||||
"code": "US-GA"
|
||||
},
|
||||
"US-GU":
|
||||
{
|
||||
"name": "Guam",
|
||||
"code": "US-GU"
|
||||
},
|
||||
"US-HI":
|
||||
{
|
||||
"name": "Hawaii",
|
||||
"code": "US-HI"
|
||||
},
|
||||
"US-ID":
|
||||
{
|
||||
"name": "Idaho",
|
||||
"code": "US-ID"
|
||||
},
|
||||
"US-IL":
|
||||
{
|
||||
"name": "Illinois",
|
||||
"code": "US-IL"
|
||||
},
|
||||
"US-IN":
|
||||
{
|
||||
"name": "Indiana",
|
||||
"code": "US-IN"
|
||||
},
|
||||
"US-IA":
|
||||
{
|
||||
"name": "Iowa",
|
||||
"code": "US-IA"
|
||||
},
|
||||
"US-KS":
|
||||
{
|
||||
"name": "Kansas",
|
||||
"code": "US-KS"
|
||||
},
|
||||
"US-KY":
|
||||
{
|
||||
"name": "Kentucky",
|
||||
"code": "US-KY"
|
||||
},
|
||||
"US-LA":
|
||||
{
|
||||
"name": "Louisiana",
|
||||
"code": "US-LA"
|
||||
},
|
||||
"US-ME":
|
||||
{
|
||||
"name": "Maine",
|
||||
"code": "US-ME"
|
||||
},
|
||||
"US-MH":
|
||||
{
|
||||
"name": "Marshall Islands",
|
||||
"code": "US-MH"
|
||||
},
|
||||
"US-MD":
|
||||
{
|
||||
"name": "Maryland",
|
||||
"code": "US-MD"
|
||||
},
|
||||
"US-MA":
|
||||
{
|
||||
"name": "Massachusetts",
|
||||
"code": "US-MA"
|
||||
},
|
||||
"US-MI":
|
||||
{
|
||||
"name": "Michigan",
|
||||
"code": "US-MI"
|
||||
},
|
||||
"US-MN":
|
||||
{
|
||||
"name": "Minnesota",
|
||||
"code": "US-MN"
|
||||
},
|
||||
"US-MS":
|
||||
{
|
||||
"name": "Mississippi",
|
||||
"code": "US-MS"
|
||||
},
|
||||
"US-MO":
|
||||
{
|
||||
"name": "Missouri",
|
||||
"code": "US-MO"
|
||||
},
|
||||
"US-MT":
|
||||
{
|
||||
"name": "Montana",
|
||||
"code": "US-MT"
|
||||
},
|
||||
"US-NE":
|
||||
{
|
||||
"name": "Nebraska",
|
||||
"code": "US-NE"
|
||||
},
|
||||
"US-NV":
|
||||
{
|
||||
"name": "Nevada",
|
||||
"code": "US-NV"
|
||||
},
|
||||
"US-NH":
|
||||
{
|
||||
"name": "New Hampshire",
|
||||
"code": "US-NH"
|
||||
},
|
||||
"US-NJ":
|
||||
{
|
||||
"name": "New Jersey",
|
||||
"code": "US-NJ"
|
||||
},
|
||||
"US-NM":
|
||||
{
|
||||
"name": "New Mexico",
|
||||
"code": "US-NM"
|
||||
},
|
||||
"US-NY":
|
||||
{
|
||||
"name": "New York",
|
||||
"code": "US-NY"
|
||||
},
|
||||
"US-NC":
|
||||
{
|
||||
"name": "North Carolina",
|
||||
"code": "US-NC"
|
||||
},
|
||||
"US-ND":
|
||||
{
|
||||
"name": "North Dakota",
|
||||
"code": "US-ND"
|
||||
},
|
||||
"US-MP":
|
||||
{
|
||||
"name": "Northern Mariana Islands",
|
||||
"code": "US-MP"
|
||||
},
|
||||
"US-OH":
|
||||
{
|
||||
"name": "Ohio",
|
||||
"code": "US-OH"
|
||||
},
|
||||
"US-OK":
|
||||
{
|
||||
"name": "Oklahoma",
|
||||
"code": "US-OK"
|
||||
},
|
||||
"US-OR":
|
||||
{
|
||||
"name": "Oregon",
|
||||
"code": "US-OR"
|
||||
},
|
||||
"US-PW":
|
||||
{
|
||||
"name": "Palau",
|
||||
"code": "US-PW"
|
||||
},
|
||||
"US-PA":
|
||||
{
|
||||
"name": "Pennsylvania",
|
||||
"code": "US-PA"
|
||||
},
|
||||
"US-PR":
|
||||
{
|
||||
"name": "Puerto Rico",
|
||||
"code": "US-PR"
|
||||
},
|
||||
"US-RI":
|
||||
{
|
||||
"name": "Rhode Island",
|
||||
"code": "US-RI"
|
||||
},
|
||||
"US-SC":
|
||||
{
|
||||
"name": "South Carolina",
|
||||
"code": "US-SC"
|
||||
},
|
||||
"US-SD":
|
||||
{
|
||||
"name": "South Dakota",
|
||||
"code": "US-SD"
|
||||
},
|
||||
"US-TN":
|
||||
{
|
||||
"name": "Tennessee",
|
||||
"code": "US-TN"
|
||||
},
|
||||
"US-TX":
|
||||
{
|
||||
"name": "Texas",
|
||||
"code": "US-TX"
|
||||
},
|
||||
"US-UT":
|
||||
{
|
||||
"name": "Utah",
|
||||
"code": "US-UT"
|
||||
},
|
||||
"US-VT":
|
||||
{
|
||||
"name": "Vermont",
|
||||
"code": "US-VT"
|
||||
},
|
||||
"US-VI":
|
||||
{
|
||||
"name": "Virgin Islands",
|
||||
"code": "US-VI"
|
||||
},
|
||||
"US-VA":
|
||||
{
|
||||
"name": "Virginia",
|
||||
"code": "US-VA"
|
||||
},
|
||||
"US-WA":
|
||||
{
|
||||
"name": "Washington",
|
||||
"code": "US-WA"
|
||||
},
|
||||
"US-WV":
|
||||
{
|
||||
"name": "West Virginia",
|
||||
"code": "US-WV"
|
||||
},
|
||||
"US-WI":
|
||||
{
|
||||
"name": "Wisconsin",
|
||||
"code": "US-WI"
|
||||
},
|
||||
"US-WY":
|
||||
{
|
||||
"name": "Wyoming",
|
||||
"code": "US-WY"
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
const glob = require('glob')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const wcmatch = require('wildcard-match')
|
||||
|
||||
function list(pattern, include = [], exclude = []) {
|
||||
return new Promise(resolve => {
|
||||
glob(pattern, function (err, files) {
|
||||
if (include.length) {
|
||||
files = files.filter(filename => include.some(item => wcmatch(item)(filename)))
|
||||
}
|
||||
|
||||
if (exclude.length) {
|
||||
files = files.filter(filename => !exclude.some(item => wcmatch(item)(filename)))
|
||||
}
|
||||
|
||||
resolve(files)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function load(filename) {
|
||||
const filePath = path.resolve(filename)
|
||||
|
||||
return require(filePath)
|
||||
}
|
||||
|
||||
function read(filename) {
|
||||
return fs.readFileSync(filename, { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
function write(filename, data) {
|
||||
const dir = path.resolve(path.dirname(filename))
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve(filename), data)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
list,
|
||||
read,
|
||||
write,
|
||||
load
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
const parser = require('epg-parser')
|
||||
const markdownInclude = require('markdown-include')
|
||||
const countries = require('./countries.json')
|
||||
const file = require('./file')
|
||||
|
||||
type EPG = {
|
||||
channels: Channel[]
|
||||
programs: Program[]
|
||||
}
|
||||
|
||||
type Country = {
|
||||
flag: string
|
||||
name: string
|
||||
code: string
|
||||
states?: State[]
|
||||
}
|
||||
|
||||
type State = {
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
type Channel = {
|
||||
id: string
|
||||
}
|
||||
|
||||
type Program = {
|
||||
channel: string
|
||||
}
|
||||
|
||||
type Guide = {
|
||||
name?: string
|
||||
flag: string
|
||||
url: string
|
||||
channelCount: number
|
||||
programCount: number
|
||||
errorCount: number
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting...')
|
||||
file
|
||||
.list('.gh-pages/guides/**/*.xml')
|
||||
.then((files: string[]) => {
|
||||
const guidesByCountry: Guide[] = []
|
||||
const guidesByUSState: Guide[] = []
|
||||
const guidesByCanadaProvince: Guide[] = []
|
||||
files.forEach((filename: string) => {
|
||||
console.log(`Loading '${filename}'...`)
|
||||
const [_, code, site]: string[] = filename.match(
|
||||
/\.gh\-pages\/guides\/(.*)\/(.*)\.epg\.xml/i
|
||||
) || ['', '', '']
|
||||
if (!code || !site) return
|
||||
|
||||
const xml: string = file.read(filename)
|
||||
const epg: EPG = parser.parse(xml)
|
||||
|
||||
if (!epg.channels.length) return
|
||||
|
||||
const log = file.read(`logs/${site}_${code}.log`)
|
||||
const errorCount = (log.match(/ERROR/gi) || []).length
|
||||
|
||||
const guide: Guide = {
|
||||
flag: '',
|
||||
url: filename.replace('.gh-pages', 'https://iptv-org.github.io/epg'),
|
||||
channelCount: epg.channels.length,
|
||||
programCount: epg.programs.length,
|
||||
errorCount
|
||||
}
|
||||
|
||||
if (code.startsWith('us-')) {
|
||||
const [_, stateCode] = code.split('-')
|
||||
const state: State | undefined = countries['us']
|
||||
? countries['us'].states[stateCode]
|
||||
: undefined
|
||||
if (!state) return
|
||||
guide.name = state.name
|
||||
guidesByUSState.push(guide)
|
||||
} else if (code.startsWith('ca-')) {
|
||||
const [_, provinceCode] = code.split('-')
|
||||
const province: State | undefined = countries['ca']
|
||||
? countries['ca'].states[provinceCode]
|
||||
: undefined
|
||||
if (!province) return
|
||||
guide.name = province.name
|
||||
guidesByCanadaProvince.push(guide)
|
||||
} else {
|
||||
const [countryCode] = code.split('-')
|
||||
const country: Country | undefined = countries[countryCode]
|
||||
if (!country) return
|
||||
guide.flag = country.flag
|
||||
guide.name = country.name
|
||||
guidesByCountry.push(guide)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Generating country table...')
|
||||
const countryTable = generateTable(guidesByCountry, ['Country', 'Channels', 'EPG', 'Status'])
|
||||
file.write('.readme/_countries.md', countryTable)
|
||||
|
||||
console.log('Generating US states table...')
|
||||
const usStatesTable = generateTable(guidesByUSState, ['State', 'Channels', 'EPG', 'Status'])
|
||||
file.write('.readme/_us-states.md', usStatesTable)
|
||||
|
||||
console.log('Generating Canada provinces table...')
|
||||
const caProvincesTable = generateTable(guidesByCanadaProvince, [
|
||||
'Province',
|
||||
'Channels',
|
||||
'EPG',
|
||||
'Status'
|
||||
])
|
||||
file.write('.readme/_ca-provinces.md', caProvincesTable)
|
||||
|
||||
console.log('Updating README.md...')
|
||||
markdownInclude.compileFiles('.readme/config.json')
|
||||
})
|
||||
.finally(() => {
|
||||
console.log('Finish')
|
||||
})
|
||||
}
|
||||
|
||||
function generateTable(guides: Guide[], header: string[]) {
|
||||
guides = sortGuides(guides)
|
||||
|
||||
let output = '<table>\n'
|
||||
|
||||
output += '\t<thead>\n\t\t<tr>'
|
||||
for (let column of header) {
|
||||
output += `<th align="left">${column}</th>`
|
||||
}
|
||||
output += '</tr>\n\t</thead>\n'
|
||||
|
||||
output += '\t<tbody>\n'
|
||||
for (let guide of guides) {
|
||||
const size = guides.filter((g: Guide) => g.name === guide.name).length
|
||||
if (!guide.name) continue
|
||||
const name = `${guide.flag} ${guide.name}`
|
||||
let root = output.indexOf(name) === -1
|
||||
const rowspan = root && size > 1 ? ` rowspan="${size}"` : ''
|
||||
let status = '🟢'
|
||||
if (guide.programCount === 0) status = '🔴'
|
||||
else if (guide.errorCount > 0) status = '🟡'
|
||||
const cell1 = root ? `<td align="left" valign="top" nowrap${rowspan}>${name}</td>` : ''
|
||||
output += `\t\t<tr>${cell1}<td align="right" nowrap>${guide.channelCount}</td><td align="left" nowrap><code>${guide.url}</code></td><td align="center">${status}</td></tr>\n`
|
||||
}
|
||||
output += '\t</tbody>\n'
|
||||
|
||||
output += '</table>'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function sortGuides(guides: Guide[]): Guide[] {
|
||||
return guides.sort((a, b) => {
|
||||
var nameA = a.name ? a.name.toLowerCase() : ''
|
||||
var nameB = b.name ? b.name.toLowerCase() : ''
|
||||
if (nameA < nameB) return -1
|
||||
if (nameA > nameB) return 1
|
||||
return b.channelCount - a.channelCount
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,6 +1,9 @@
|
|||
const axios = require('axios')
|
||||
const cheerio = require('cheerio')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
module.exports = {
|
||||
site: 'nowplayer.now.com',
|
||||
|
|
2
tests/__data__/.gitignore
vendored
Normal file
2
tests/__data__/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
output/
|
||||
temp/
|
1
tests/__data__/expected/api/channels.json
Normal file
1
tests/__data__/expected/api/channels.json
Normal file
|
@ -0,0 +1 @@
|
|||
[{"id":"BravoEast.us","name":["Bravo East"],"logo":"https://www.directv.com/images/logos/channels/dark/large/579.png","country":"US","guides":["https://iptv-org.github.io/epg/guides/us/directv.com.epg.xml"]},{"id":"CNNInternationalEurope.us","name":["CNN International Europe","CNN Int"],"logo":"https://www.magticom.ge/images/channels/MjAxOC8wOS8xMC9lZmJhNWU5Yy0yMmNiLTRkMTAtOWY5Ny01ODM0MzY0ZTg0MmEuanBn.jpg","country":"US","guides":["https://iptv-org.github.io/epg/guides/fr/chaines-tv.orange.fr.epg.xml","https://iptv-org.github.io/epg/guides/ge/magticom.ge.epg.xml"]},{"id":"MNetMovies2.za","name":["M-Net Movies 2"],"logo":"https://rndcdn.dstv.com/dstvcms/2020/08/31/M-Net_Movies_2_Logo_4-3_lightbackground_xlrg.png","country":"ZA","guides":["https://iptv-org.github.io/epg/guides/zw/dstv.com.epg.xml"]}]
|
1
tests/__data__/expected/api/programs.json
Normal file
1
tests/__data__/expected/api/programs.json
Normal file
File diff suppressed because one or more lines are too long
35
tests/__data__/expected/guides/us/magticom.ge.epg.xml
Normal file
35
tests/__data__/expected/guides/us/magticom.ge.epg.xml
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?><tv>
|
||||
<channel id="CNNInternationalEurope.us"><display-name>CNN Int</display-name><icon src="https://www.magticom.ge/images/channels/MjAxOC8wOS8xMC9lZmJhNWU5Yy0yMmNiLTRkMTAtOWY5Ny01ODM0MzY0ZTg0MmEuanBn.jpg"/><url>https://magticom.ge</url></channel>
|
||||
<programme start="20220110000000 +0000" stop="20220110010000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">CNN Newsroom Sunday</title><desc lang="ru">Свежая мировая информационная сводка от CNN. О политике, экономике, общественной жизни, культуре, спорте.</desc></programme>
|
||||
<programme start="20220110010000 +0000" stop="20220110020000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Fareed Zakaria GPS</title><desc lang="ru">Интервью с главными игроками мировой политики.</desc></programme>
|
||||
<programme start="20220110020000 +0000" stop="20220110023000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">African Voices Changemakers. 114-я серия</title><desc lang="ru">114-я серия. Африка сегодня - люди, новости, события.</desc></programme>
|
||||
<programme start="20220110023000 +0000" stop="20220110024500 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Marketplace Africa. 549-я серия</title><desc lang="ru">549-я серия. Информационная передача об экономических событиях африканского региона. Анализируются проблемы, даются экономические прогнозы.</desc></programme>
|
||||
<programme start="20220110024500 +0000" stop="20220110030000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Marketplace Africa. 548-я серия</title><desc lang="ru">548-я серия. Информационная передача об экономических событиях африканского региона. Анализируются проблемы, даются экономические прогнозы.</desc></programme>
|
||||
<programme start="20220110030000 +0000" stop="20220110033000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">TBD</title><desc lang="ru">Информационно-познавательный проект CNN.</desc></programme>
|
||||
<programme start="20220110033000 +0000" stop="20220110040000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Inside Africa. 586-я серия</title><desc lang="ru">586-я серия. Своеобразное "путешествие" по Африке - почувствуйте все разнообразие культур различных стран и регионов континента.</desc></programme>
|
||||
<programme start="20220110040000 +0000" stop="20220110044500 +0000" channel="CNNInternationalEurope.us"><title lang="ru">CNN Newsroom with Michael Holmes</title><desc lang="ru">Обзор самых важных и актуальных новостей и событий из жизни страны и мира.</desc></programme>
|
||||
<programme start="20220110044500 +0000" stop="20220110050000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">World Sport</title><desc lang="ru">Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.</desc></programme>
|
||||
<programme start="20220110050000 +0000" stop="20220110060000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">CNN Newsroom with Michael Holmes</title><desc lang="ru">Обзор самых важных и актуальных новостей и событий из жизни страны и мира.</desc></programme>
|
||||
<programme start="20220110060000 +0000" stop="20220110064500 +0000" channel="CNNInternationalEurope.us"><title lang="ru">CNN Newsroom with Robyn Curnow</title><desc lang="ru">Обзор самых важных и актуальных новостей и событий из жизни страны и мира.</desc></programme>
|
||||
<programme start="20220110064500 +0000" stop="20220110070000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">World Sport</title><desc lang="ru">Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.</desc></programme>
|
||||
<programme start="20220110070000 +0000" stop="20220110090000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">CNN Newsroom with Rosemary Church</title><desc lang="ru">Свежая мировая информационная сводка от CNN. О политике, экономике, общественной жизни, культуре, спорте.</desc></programme>
|
||||
<programme start="20220110090000 +0000" stop="20220110100000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Early Start</title><desc lang="ru">Новости дня с Кристиной Романс и Дейвом Бриггсом.</desc></programme>
|
||||
<programme start="20220110100000 +0000" stop="20220110123000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">New Day</title><desc lang="ru">Свежий обзор событий в стране и мире.</desc></programme>
|
||||
<programme start="20220110123000 +0000" stop="20220110130000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">World Sport</title><desc lang="ru">Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.</desc></programme>
|
||||
<programme start="20220110130000 +0000" stop="20220110140000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">First Move with Julia Chatterley</title><desc lang="ru">Несколько больших историй, связанных с открытием рынков в США.</desc></programme>
|
||||
<programme start="20220110140000 +0000" stop="20220110144500 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Connect the World</title><desc lang="ru">Актуальная мировая информация с разных континентов.</desc></programme>
|
||||
<programme start="20220110144500 +0000" stop="20220110150000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">World Sport</title><desc lang="ru">Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.</desc></programme>
|
||||
<programme start="20220110150000 +0000" stop="20220110160000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Connect the World</title><desc lang="ru">Актуальная мировая информация с разных континентов.</desc></programme>
|
||||
<programme start="20220110160000 +0000" stop="20220110164500 +0000" channel="CNNInternationalEurope.us"><title lang="ru">One World with Zain Asher</title><desc lang="ru">Освещаются важные новости с каждого континента, от политики и текущих дел до социальных вопросов и многого другого.</desc></programme>
|
||||
<programme start="20220110164500 +0000" stop="20220110170000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Marketplace Africa. 549-я серия</title><desc lang="ru">549-я серия. Информационная передача об экономических событиях африканского региона. Анализируются проблемы, даются экономические прогнозы.</desc></programme>
|
||||
<programme start="20220110170000 +0000" stop="20220110180000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Amanpour</title><desc lang="ru">Сводка новостей от знаменитой ведущей канала CNN.</desc></programme>
|
||||
<programme start="20220110180000 +0000" stop="20220110190000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Hala Gorani Tonight</title><desc lang="ru">Используя свой 25-летний журналистский опыт, Хала Горани будет освещать ключевые события в картине дня посредством диалога с гостями и экспертами-аналитиками.</desc></programme>
|
||||
<programme start="20220110190000 +0000" stop="20220110194500 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Quest Means Business</title><desc lang="ru">Ричард Квест возглавляет группу экспертов и корреспондентов, чтобы предоставить актуальные факты, цифры и анализ из делового мира.</desc></programme>
|
||||
<programme start="20220110194500 +0000" stop="20220110200000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Connecting Africa. 114-я серия</title><desc lang="ru">114-я серия. Проект, рассказывающий о людях и компаниях, которые совершают революцию в африканском бизнесе, и о тех, кто объединяет континент, выступая за свободную торговлю в Африке.</desc></programme>
|
||||
<programme start="20220110200000 +0000" stop="20220110210000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">The Lead with Jake Tapper</title><desc lang="ru">Оперативная сводка новостей страны и мира.</desc></programme>
|
||||
<programme start="20220110210000 +0000" stop="20220110213000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">The Global Brief with Bianca Nobilo</title><desc lang="ru">Global Brief с Бьянкой Нобило проницательно исследует меняющийся мир для меняющейся аудитории, обеспечивая непревзойденную глубину и качество для занятых зрителей в быстро меняющемся мире.</desc></programme>
|
||||
<programme start="20220110213000 +0000" stop="20220110220000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">World Sport</title><desc lang="ru">Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.</desc></programme>
|
||||
<programme start="20220110220000 +0000" stop="20220110230000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">The Situation Room with Wolf Blitzer</title><desc lang="ru">Командный центр новостей, политики и неординарных репортажей со всего мира.</desc></programme>
|
||||
<programme start="20220110230000 +0000" stop="20220111000000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Erin Burnett OutFront</title><desc lang="ru">Обсуждение самых важных мировых тем в эфире канала CNN.</desc></programme>
|
||||
<programme start="20220111000000 +0000" stop="20220111010000 +0000" channel="CNNInternationalEurope.us"><title lang="ru">Anderson Cooper 360</title><desc lang="ru">Уникальный взгляд Андерсона Купера на главные события мира.</desc></programme>
|
||||
</tv>
|
17
tests/__data__/expected/guides/za/dstv.com.epg.xml
Normal file
17
tests/__data__/expected/guides/za/dstv.com.epg.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?><tv>
|
||||
<channel id="MNetMovies2.za"><display-name>M-Net Movies 2</display-name><icon src="https://rndcdn.dstv.com/dstvcms/2020/08/31/M-Net_Movies_2_Logo_4-3_lightbackground_xlrg.png"/><url>https://dstv.com</url></channel>
|
||||
<programme start="20220109205500 +0000" stop="20220109223500 +0000" channel="MNetMovies2.za"><title lang="en">Fatman</title></programme>
|
||||
<programme start="20220109223500 +0000" stop="20220110005500 +0000" channel="MNetMovies2.za"><title lang="en">Motherless Brooklyn</title></programme>
|
||||
<programme start="20220110005500 +0000" stop="20220110024500 +0000" channel="MNetMovies2.za"><title lang="en">The Water Diviner</title></programme>
|
||||
<programme start="20220110024500 +0000" stop="20220110045000 +0000" channel="MNetMovies2.za"><title lang="en">Outbreak</title></programme>
|
||||
<programme start="20220110045000 +0000" stop="20220110063500 +0000" channel="MNetMovies2.za"><title lang="en">Paranoia</title></programme>
|
||||
<programme start="20220110063500 +0000" stop="20220110075500 +0000" channel="MNetMovies2.za"><title lang="en">Beyond The Line</title></programme>
|
||||
<programme start="20220110075500 +0000" stop="20220110101000 +0000" channel="MNetMovies2.za"><title lang="en">Backdraft</title></programme>
|
||||
<programme start="20220110101000 +0000" stop="20220110113500 +0000" channel="MNetMovies2.za"><title lang="en">Mafia</title></programme>
|
||||
<programme start="20220110113500 +0000" stop="20220110134500 +0000" channel="MNetMovies2.za"><title lang="en">12 Strong</title></programme>
|
||||
<programme start="20220110134500 +0000" stop="20220110154000 +0000" channel="MNetMovies2.za"><title lang="en">Robin Hood</title></programme>
|
||||
<programme start="20220110154000 +0000" stop="20220110171300 +0000" channel="MNetMovies2.za"><title lang="en">The Scorpion King</title></programme>
|
||||
<programme start="20220110171300 +0000" stop="20220110190000 +0000" channel="MNetMovies2.za"><title lang="en">The Last Witch Hunter</title></programme>
|
||||
<programme start="20220110190000 +0000" stop="20220110204000 +0000" channel="MNetMovies2.za"><title lang="en">Force Of Nature</title></programme>
|
||||
<programme start="20220110204000 +0000" stop="20220110214000 +0000" channel="MNetMovies2.za"><title lang="en">Bad Boys For Life</title></programme>
|
||||
</tv>
|
90
tests/__data__/expected/readme.md
Normal file
90
tests/__data__/expected/readme.md
Normal file
|
@ -0,0 +1,90 @@
|
|||
# EPG
|
||||
|
||||

|
||||
|
||||
EPG (Electronic Program Guide) for thousands of TV channels collected from different sources.
|
||||
|
||||
## Usage
|
||||
|
||||
To load a program guide, all you need to do is copy the link to one or more of the guides from the list below and paste it into your favorite player.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Country</th><th align="left">Channels</th><th align="left">EPG</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left" valign="top" nowrap>🇿🇦 South Africa</td><td align="right">1</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/za/dstv.com.epg.xml</code></td></tr>
|
||||
<tr><td align="left" valign="top" nowrap rowspan="2">🇺🇸 United States</td><td align="right">372</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/us/tvtv.us.epg.xml</code></td></tr>
|
||||
<tr><td align="right">74</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/us/magticom.ge.epg.xml</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### US States
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">State</th><th align="left">Channels</th><th align="left">EPG</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left" valign="top" nowrap rowspan="3">Puerto Rico</td><td align="right">14</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/us-pr/tvtv.us.epg.xml</code></td></tr>
|
||||
<tr><td align="right">7</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/us-pr/gatotv.com.epg.xml</code></td></tr>
|
||||
<tr><td align="right">1</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/us-pr/directv.com.epg.xml</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Provinces of Canada
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th align="left">Province</th><th align="left">Channels</th><th align="left">EPG</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td align="left" valign="top" nowrap>Newfoundland and Labrador</td><td align="right">1</td><td align="left" nowrap><code>https://iptv-org.github.io/epg/guides/ca-nl/tvtv.us.epg.xml</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## List of supported channels
|
||||
|
||||
https://iptv-org.github.io/epg/index.html
|
||||
|
||||
## For Developers
|
||||
|
||||
You can also get a list of all available channels and their codes in JSON format by sending a GET request to:
|
||||
|
||||
```
|
||||
https://iptv-org.github.io/epg/api/channels.json
|
||||
```
|
||||
|
||||
If successful, you should get the following response:
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<br>
|
||||
|
||||
```
|
||||
[
|
||||
...
|
||||
{
|
||||
"id": "CNNUSA.us",
|
||||
"name": [
|
||||
"CNN USA"
|
||||
],
|
||||
"logo": "https://cdn.tvpassport.com/image/station/100x100/cnn.png",
|
||||
"country": "US",
|
||||
"guides": [
|
||||
"https://iptv-org.github.io/epg/guides/tvtv.us.guide.xml",
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Contribution
|
||||
|
||||
If you find a bug or want to contribute to the code or documentation, you can help by submitting an [issue](https://github.com/iptv-org/epg/issues) or a [pull request](https://github.com/iptv-org/epg/pulls).
|
4
tests/__data__/input/database/channels.db
Normal file
4
tests/__data__/input/database/channels.db
Normal file
|
@ -0,0 +1,4 @@
|
|||
{"lang":"en","xmltv_id":"BravoEast.us","site_id":"237","logo":"https://www.directv.com/images/logos/channels/dark/large/579.png","name":"Bravo East","site":"directv.com","channelsPath":"sites/directv.com/directv.com_us.channels.xml","configPath":"sites/directv.com/directv.com.config.js","gid":"us","cluster_id":84,"country":"US","_id":"00AluKCrCnfgrl8W"}
|
||||
{"lang":"fr","country":"US","xmltv_id":"CNNInternationalEurope.us","site_id":"53","logo":null,"name":"CNN International Europe","site":"chaines-tv.orange.fr","channelsPath":"sites/chaines-tv.orange.fr/chaines-tv.orange.fr_fr.channels.xml","configPath":"tests/__data__/input/sites/example.com.config.js","gid":"fr","cluster_id":1,"_id":"0Wefq0oMR3feCcuY"}
|
||||
{"lang":"ru","country":"US","xmltv_id":"CNNInternationalEurope.us","site_id":"140","logo":"https://www.magticom.ge/images/channels/MjAxOC8wOS8xMC9lZmJhNWU5Yy0yMmNiLTRkMTAtOWY5Ny01ODM0MzY0ZTg0MmEuanBn.jpg","name":"CNN Int","site":"magticom.ge","channelsPath":"sites/magticom.ge/magticom.ge_ge.channels.xml","configPath":"tests/__data__/input/sites/example.com.config.js","gid":"ge","cluster_id":1,"_id":"1XzrxNkSF2AQNBrT"}
|
||||
{"lang":"en","country":"ZA","xmltv_id":"MNetMovies2.za","site_id":"404a052b-3dea-4cac-a19c-de9a7d6f191d#MAP","logo":"https://rndcdn.dstv.com/dstvcms/2020/08/31/M-Net_Movies_2_Logo_4-3_lightbackground_xlrg.png","name":"M-Net Movies 2","site":"dstv.com","channelsPath":"sites/dstv.com/dstv.com_zw.channels.xml","configPath":"sites/dstv.com/dstv.com.config.js","gid":"zw","cluster_id":120,"_id":"1lnhXpN7g0ER5XwN"}
|
46
tests/__data__/input/database/programs.db
Normal file
46
tests/__data__/input/database/programs.db
Normal file
|
@ -0,0 +1,46 @@
|
|||
{"title":"World Sport","description":"Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641825900,"stop":1641826800,"site":"magticom.ge","gid":"us","country":"US","_id":"12AJc0GeEJE9p4c3"}
|
||||
{"title":"Connecting Africa. 114-я серия","description":"114-я серия. Проект, рассказывающий о людях и компаниях, которые совершают революцию в африканском бизнесе, и о тех, кто объединяет континент, выступая за свободную торговлю в Африке.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641843900,"stop":1641844800,"site":"magticom.ge","gid":"us","country":"US","_id":"1dxcT34nyxzOlxBL"}
|
||||
{"title":"Connect the World","description":"Актуальная мировая информация с разных континентов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641826800,"stop":1641830400,"site":"magticom.ge","gid":"us","country":"US","_id":"2uJe4w2lgvjNOXo0"}
|
||||
{"title":"The Lead with Jake Tapper","description":"Оперативная сводка новостей страны и мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641844800,"stop":1641848400,"site":"magticom.ge","gid":"us","country":"US","_id":"6As6GzEVhb3OWM0M"}
|
||||
{"title":"World Sport","description":"Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641817800,"stop":1641819600,"site":"magticom.ge","gid":"us","country":"US","_id":"6DXKlITWehX1Jx4F"}
|
||||
{"title":"CNN Newsroom with Michael Holmes","description":"Обзор самых важных и актуальных новостей и событий из жизни страны и мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641790800,"stop":1641794400,"site":"magticom.ge","gid":"us","country":"US","_id":"AadPdMZ3s72y8NMk"}
|
||||
{"title":"The Situation Room with Wolf Blitzer","description":"Командный центр новостей, политики и неординарных репортажей со всего мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641852000,"stop":1641855600,"site":"magticom.ge","gid":"us","country":"US","_id":"Az3ABKy3HnE7sJZk"}
|
||||
{"title":"One World with Zain Asher","description":"Освещаются важные новости с каждого континента, от политики и текущих дел до социальных вопросов и многого другого.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641830400,"stop":1641833100,"site":"magticom.ge","gid":"us","country":"US","_id":"DMurxgt5OD0E9OIE"}
|
||||
{"title":"TBD","description":"Информационно-познавательный проект CNN.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641783600,"stop":1641785400,"site":"magticom.ge","gid":"us","country":"US","_id":"HQJqM2kIa77llWbC"}
|
||||
{"title":"Marketplace Africa. 548-я серия","description":"548-я серия. Информационная передача об экономических событиях африканского региона. Анализируются проблемы, даются экономические прогнозы.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641782700,"stop":1641783600,"site":"magticom.ge","gid":"us","country":"US","_id":"Jn3khh5n9Brkxq4U"}
|
||||
{"title":"CNN Newsroom with Michael Holmes","description":"Обзор самых важных и актуальных новостей и событий из жизни страны и мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641787200,"stop":1641789900,"site":"magticom.ge","gid":"us","country":"US","_id":"KcrIoQTXtUdw74sO"}
|
||||
{"title":"The Global Brief with Bianca Nobilo","description":"Global Brief с Бьянкой Нобило проницательно исследует меняющийся мир для меняющейся аудитории, обеспечивая непревзойденную глубину и качество для занятых зрителей в быстро меняющемся мире.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641848400,"stop":1641850200,"site":"magticom.ge","gid":"us","country":"US","_id":"LGD7WmQogDRxZn01"}
|
||||
{"title":"CNN Newsroom with Rosemary Church","description":"Свежая мировая информационная сводка от CNN. О политике, экономике, общественной жизни, культуре, спорте.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641798000,"stop":1641805200,"site":"magticom.ge","gid":"us","country":"US","_id":"LyCBivUTdZFW9X53"}
|
||||
{"title":"Marketplace Africa. 549-я серия","description":"549-я серия. Информационная передача об экономических событиях африканского региона. Анализируются проблемы, даются экономические прогнозы.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641833100,"stop":1641834000,"site":"magticom.ge","gid":"us","country":"US","_id":"PbrZinuZKgBHqDVj"}
|
||||
{"title":"African Voices Changemakers. 114-я серия","description":"114-я серия. Африка сегодня - люди, новости, события.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641780000,"stop":1641781800,"site":"magticom.ge","gid":"us","country":"US","_id":"SvrCK31v78V5y7EA"}
|
||||
{"title":"Anderson Cooper 360","description":"Уникальный взгляд Андерсона Купера на главные события мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641859200,"stop":1641862800,"site":"magticom.ge","gid":"us","country":"US","_id":"TFGrOFJGkaOs9pU7"}
|
||||
{"title":"World Sport","description":"Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641850200,"stop":1641852000,"site":"magticom.ge","gid":"us","country":"US","_id":"UynlLeT41MsjFElg"}
|
||||
{"title":"New Day","description":"Свежий обзор событий в стране и мире.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641808800,"stop":1641817800,"site":"magticom.ge","gid":"us","country":"US","_id":"UyvhQ4wRNq5d5XRd"}
|
||||
{"title":"Amanpour","description":"Сводка новостей от знаменитой ведущей канала CNN.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641834000,"stop":1641837600,"site":"magticom.ge","gid":"us","country":"US","_id":"WbsOCkmPH5gjmo4M"}
|
||||
{"title":"Early Start","description":"Новости дня с Кристиной Романс и Дейвом Бриггсом.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641805200,"stop":1641808800,"site":"magticom.ge","gid":"us","country":"US","_id":"YB96P2mMO4TA0pID"}
|
||||
{"title":"World Sport","description":"Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641789900,"stop":1641790800,"site":"magticom.ge","gid":"us","country":"US","_id":"aDdCAlgqLG2yxM1m"}
|
||||
{"title":"CNN Newsroom Sunday","description":"Свежая мировая информационная сводка от CNN. О политике, экономике, общественной жизни, культуре, спорте.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641772800,"stop":1641776400,"site":"magticom.ge","gid":"us","country":"US","_id":"aYCk87dUOAkCJE9x"}
|
||||
{"title":"Fareed Zakaria GPS","description":"Интервью с главными игроками мировой политики.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641776400,"stop":1641780000,"site":"magticom.ge","gid":"us","country":"US","_id":"c1nCoWVetBZ3mn5q"}
|
||||
{"title":"Inside Africa. 586-я серия","description":"586-я серия. Своеобразное \"путешествие\" по Африке - почувствуйте все разнообразие культур различных стран и регионов континента.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641785400,"stop":1641787200,"site":"magticom.ge","gid":"us","country":"US","_id":"goaDr7BsGGm3LCfz"}
|
||||
{"title":"CNN Newsroom with Robyn Curnow","description":"Обзор самых важных и актуальных новостей и событий из жизни страны и мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641794400,"stop":1641797100,"site":"magticom.ge","gid":"us","country":"US","_id":"nixd3gRF1S1K0ZOs"}
|
||||
{"title":"Marketplace Africa. 549-я серия","description":"549-я серия. Информационная передача об экономических событиях африканского региона. Анализируются проблемы, даются экономические прогнозы.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641781800,"stop":1641782700,"site":"magticom.ge","gid":"us","country":"US","_id":"r1b8EvZc0tYs88ga"}
|
||||
{"title":"Erin Burnett OutFront","description":"Обсуждение самых важных мировых тем в эфире канала CNN.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641855600,"stop":1641859200,"site":"magticom.ge","gid":"us","country":"US","_id":"sIQtUtowtATc7dLj"}
|
||||
{"title":"Connect the World","description":"Актуальная мировая информация с разных континентов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641823200,"stop":1641825900,"site":"magticom.ge","gid":"us","country":"US","_id":"tXBIZ2BZBIkhnoTZ"}
|
||||
{"title":"Quest Means Business","description":"Ричард Квест возглавляет группу экспертов и корреспондентов, чтобы предоставить актуальные факты, цифры и анализ из делового мира.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641841200,"stop":1641843900,"site":"magticom.ge","gid":"us","country":"US","_id":"xlE5epkjzdfUQpXO"}
|
||||
{"title":"First Move with Julia Chatterley","description":"Несколько больших историй, связанных с открытием рынков в США.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641819600,"stop":1641823200,"site":"magticom.ge","gid":"us","country":"US","_id":"yEVXucyUomVmktMF"}
|
||||
{"title":"Hala Gorani Tonight","description":"Используя свой 25-летний журналистский опыт, Хала Горани будет освещать ключевые события в картине дня посредством диалога с гостями и экспертами-аналитиками.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641837600,"stop":1641841200,"site":"magticom.ge","gid":"us","country":"US","_id":"yPgmYrWwfxHW3WUA"}
|
||||
{"title":"World Sport","description":"Все о главных спортивных событиях мира. Обзоры самых важных спортивных событий, аналитика, мнения экспертов.","category":null,"icon":null,"channel":"CNNInternationalEurope.us","lang":"ru","start":1641797100,"stop":1641798000,"site":"magticom.ge","gid":"us","country":"US","_id":"zX70wOz5drExRTJX"}
|
||||
{"title":"Robin Hood","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641822300,"stop":1641829200,"site":"dstv.com","gid":"za","country":"ZA","_id":"1AoKArQw6MxP6pVU"}
|
||||
{"title":"The Water Diviner","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641776100,"stop":1641782700,"site":"dstv.com","gid":"za","country":"ZA","_id":"6v7w0SB4IlnfEEu3"}
|
||||
{"title":"Bad Boys For Life","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641847200,"stop":1641850800,"site":"dstv.com","gid":"za","country":"ZA","_id":"83VRYvggmyfCzkOm"}
|
||||
{"title":"12 Strong","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641814500,"stop":1641822300,"site":"dstv.com","gid":"za","country":"ZA","_id":"DbjwscjIuVDY8TPx"}
|
||||
{"title":"Backdraft","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641801300,"stop":1641809400,"site":"dstv.com","gid":"za","country":"ZA","_id":"IwuwkjCKqWvio7ba"}
|
||||
{"title":"Force Of Nature","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641841200,"stop":1641847200,"site":"dstv.com","gid":"za","country":"ZA","_id":"LP56HczEup0ed3Xx"}
|
||||
{"title":"Mafia","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641809400,"stop":1641814500,"site":"dstv.com","gid":"za","country":"ZA","_id":"MM9DPxERAgGGak39"}
|
||||
{"title":"The Last Witch Hunter","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641834780,"stop":1641841200,"site":"dstv.com","gid":"za","country":"ZA","_id":"MciJOpN3YCodj6Na"}
|
||||
{"title":"Beyond The Line","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641796500,"stop":1641801300,"site":"dstv.com","gid":"za","country":"ZA","_id":"ZKA2s6QrM0xRrfGz"}
|
||||
{"title":"Paranoia","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641790200,"stop":1641796500,"site":"dstv.com","gid":"za","country":"ZA","_id":"ZpdIZeSRhPycDX9D"}
|
||||
{"title":"The Scorpion King","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641829200,"stop":1641834780,"site":"dstv.com","gid":"za","country":"ZA","_id":"doO4Lh1pAt6L6wHa"}
|
||||
{"title":"Fatman","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641761700,"stop":1641767700,"site":"dstv.com","gid":"za","country":"ZA","_id":"fHahGuzHnU7xVEJX"}
|
||||
{"title":"Outbreak","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641782700,"stop":1641790200,"site":"dstv.com","gid":"za","country":"ZA","_id":"mkvcMP4FMwL2a5ax"}
|
||||
{"title":"Motherless Brooklyn","description":null,"category":null,"icon":null,"channel":"MNetMovies2.za","lang":"en","start":1641767700,"stop":1641776100,"site":"dstv.com","gid":"za","country":"ZA","_id":"nxTIAJsBwyXztRun"}
|
4
tests/__data__/input/readme.json
Normal file
4
tests/__data__/input/readme.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"build" : "tests/__data__/output/readme.md",
|
||||
"files" : ["./.readme/template.md"]
|
||||
}
|
12
tests/__data__/input/sites/example.com.config.js
Normal file
12
tests/__data__/input/sites/example.com.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
site: 'example.com',
|
||||
url() {
|
||||
return `https://example.com`
|
||||
},
|
||||
logo() {
|
||||
return 'https://example.com/logo.png'
|
||||
},
|
||||
parser() {
|
||||
return []
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<site site="example.com">
|
||||
<channels>
|
||||
<channel lang="ru" xmltv_id="CNNInternationalEurope.us" site_id="140">CNN International Europe</channel>
|
||||
</channels>
|
||||
</site>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<site site="example.com">
|
||||
<channels>
|
||||
<channel lang="en" xmltv_id="1Plus2.lv" site_id="1341">1+2</channel>
|
||||
</channels>
|
||||
</site>
|
59
tests/commands/create-database.test.js
Normal file
59
tests/commands/create-database.test.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
|
||||
execSync(
|
||||
'DB_DIR=tests/__data__/output/database node scripts/commands/create-database.js --channels=tests/__data__/input/sites/*.channels.xml --max-clusters=1',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
it('can create channels database', () => {
|
||||
const output = content('tests/__data__/output/database/channels.db')
|
||||
|
||||
expect(output).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
lang: 'en',
|
||||
country: 'LV',
|
||||
xmltv_id: '1Plus2.lv',
|
||||
site_id: '1341',
|
||||
name: '1+2',
|
||||
site: 'example.com',
|
||||
channelsPath: 'tests/__data__/input/sites/example.com_en-ee.channels.xml',
|
||||
configPath: 'tests/__data__/input/sites/example.com.config.js',
|
||||
gid: 'en-ee',
|
||||
cluster_id: 1
|
||||
}),
|
||||
expect.objectContaining({
|
||||
lang: 'ru',
|
||||
country: 'US',
|
||||
xmltv_id: 'CNNInternationalEurope.us',
|
||||
site_id: '140',
|
||||
name: 'CNN International Europe',
|
||||
site: 'example.com',
|
||||
channelsPath: 'tests/__data__/input/sites/example.com_ca-nl.channels.xml',
|
||||
configPath: 'tests/__data__/input/sites/example.com.config.js',
|
||||
gid: 'ca-nl',
|
||||
cluster_id: 1
|
||||
})
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
function content(filepath) {
|
||||
const data = fs.readFileSync(path.resolve(filepath), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
return data
|
||||
.split('\n')
|
||||
.filter(l => l)
|
||||
.map(l => {
|
||||
return JSON.parse(l)
|
||||
})
|
||||
}
|
28
tests/commands/create-matrix.test.js
Normal file
28
tests/commands/create-matrix.test.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
fs.mkdirSync('tests/__data__/temp/database', { recursive: true })
|
||||
fs.copyFileSync(
|
||||
'tests/__data__/input/database/channels.db',
|
||||
'tests/__data__/temp/database/channels.db'
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
})
|
||||
|
||||
it('can create valid matrix', () => {
|
||||
const result = execSync(
|
||||
'DB_DIR=tests/__data__/temp/database node scripts/commands/create-matrix.js',
|
||||
{
|
||||
encoding: 'utf8'
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toBe('::set-output name=matrix::{"cluster_id":[1,84,120]}\n')
|
||||
})
|
62
tests/commands/load-cluster.test.js
Normal file
62
tests/commands/load-cluster.test.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
fs.mkdirSync('tests/__data__/temp/database', { recursive: true })
|
||||
fs.copyFileSync(
|
||||
'tests/__data__/input/database/channels.db',
|
||||
'tests/__data__/temp/database/channels.db'
|
||||
)
|
||||
|
||||
execSync(
|
||||
'DB_DIR=tests/__data__/temp/database LOGS_DIR=tests/__data__/output/logs node scripts/commands/load-cluster.js --cluster-id=1 --timeout=10000',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
it('can load cluster', () => {
|
||||
const output = content('tests/__data__/output/logs/load-cluster/cluster_1.log')
|
||||
|
||||
expect(output[0]).toMatchObject({
|
||||
_id: '0Wefq0oMR3feCcuY',
|
||||
site: 'chaines-tv.orange.fr',
|
||||
country: 'US',
|
||||
gid: 'fr'
|
||||
})
|
||||
|
||||
expect(output[1]).toMatchObject({
|
||||
_id: '1XzrxNkSF2AQNBrT',
|
||||
site: 'magticom.ge',
|
||||
country: 'US',
|
||||
gid: 'ge'
|
||||
})
|
||||
|
||||
const database = content('tests/__data__/temp/database/channels.db')
|
||||
|
||||
expect(database[1]).toMatchObject({
|
||||
_id: '0Wefq0oMR3feCcuY',
|
||||
logo: 'https://example.com/logo.png'
|
||||
})
|
||||
|
||||
expect(database[2]).toMatchObject({
|
||||
_id: '1XzrxNkSF2AQNBrT',
|
||||
logo: 'https://www.magticom.ge/images/channels/MjAxOC8wOS8xMC9lZmJhNWU5Yy0yMmNiLTRkMTAtOWY5Ny01ODM0MzY0ZTg0MmEuanBn.jpg'
|
||||
})
|
||||
})
|
||||
|
||||
function content(filepath) {
|
||||
const data = fs.readFileSync(path.resolve(filepath), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
return data
|
||||
.split('\n')
|
||||
.filter(l => l)
|
||||
.map(l => {
|
||||
return JSON.parse(l)
|
||||
})
|
||||
}
|
45
tests/commands/save-results.test.js
Normal file
45
tests/commands/save-results.test.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
|
||||
execSync(
|
||||
'DB_DIR=tests/__data__/output/database LOGS_DIR=tests/__data__/input/logs node scripts/commands/save-results.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
it('can save results to database', () => {
|
||||
const output = content('tests/__data__/output/database/programs.db')
|
||||
|
||||
expect(Object.keys(output[0]).sort()).toEqual([
|
||||
'_id',
|
||||
'category',
|
||||
'channel',
|
||||
'country',
|
||||
'description',
|
||||
'gid',
|
||||
'icon',
|
||||
'lang',
|
||||
'site',
|
||||
'start',
|
||||
'stop',
|
||||
'title'
|
||||
])
|
||||
})
|
||||
|
||||
function content(filepath) {
|
||||
const data = fs.readFileSync(path.resolve(filepath), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
return data
|
||||
.split('\n')
|
||||
.filter(l => l)
|
||||
.map(l => {
|
||||
return JSON.parse(l)
|
||||
})
|
||||
}
|
37
tests/commands/update-api.test.js
Normal file
37
tests/commands/update-api.test.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
fs.mkdirSync('tests/__data__/temp/database', { recursive: true })
|
||||
fs.copyFileSync(
|
||||
'tests/__data__/input/database/channels.db',
|
||||
'tests/__data__/temp/database/channels.db'
|
||||
)
|
||||
|
||||
execSync(
|
||||
'DB_DIR=tests/__data__/temp/database API_DIR=tests/__data__/output/api node scripts/commands/update-api.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
})
|
||||
|
||||
it('can generate channels.json', () => {
|
||||
const output = content('tests/__data__/output/api/channels.json')
|
||||
const expected = content('tests/__data__/expected/api/channels.json')
|
||||
|
||||
expect(output).toBe(expected)
|
||||
})
|
||||
|
||||
function content(filepath) {
|
||||
const data = fs.readFileSync(path.resolve(filepath), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
return JSON.stringify(data)
|
||||
}
|
51
tests/commands/update-guides.test.js
Normal file
51
tests/commands/update-guides.test.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
fs.mkdirSync('tests/__data__/temp/database', { recursive: true })
|
||||
fs.copyFileSync(
|
||||
'tests/__data__/input/database/channels.db',
|
||||
'tests/__data__/temp/database/channels.db'
|
||||
)
|
||||
fs.copyFileSync(
|
||||
'tests/__data__/input/database/programs.db',
|
||||
'tests/__data__/temp/database/programs.db'
|
||||
)
|
||||
|
||||
execSync(
|
||||
'DB_DIR=tests/__data__/temp/database PUBLIC_DIR=tests/__data__/output LOGS_DIR=tests/__data__/output/logs node scripts/commands/update-guides.js',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmdirSync('tests/__data__/temp', { recursive: true })
|
||||
})
|
||||
|
||||
it('can generate /guides', () => {
|
||||
const output1 = content('tests/__data__/output/guides/us/magticom.ge.epg.xml')
|
||||
const expected1 = content('tests/__data__/expected/guides/us/magticom.ge.epg.xml')
|
||||
|
||||
expect(output1).toBe(expected1)
|
||||
|
||||
const output2 = content('tests/__data__/output/guides/za/dstv.com.epg.xml')
|
||||
const expected2 = content('tests/__data__/expected/guides/za/dstv.com.epg.xml')
|
||||
|
||||
expect(output2).toBe(expected2)
|
||||
|
||||
const output3 = content('tests/__data__/output/logs/update-guides.log')
|
||||
const expected3 = content('tests/__data__/expected/logs/update-guides.log')
|
||||
|
||||
expect(output3).toBe(expected3)
|
||||
})
|
||||
|
||||
function content(filepath) {
|
||||
const data = fs.readFileSync(path.resolve(filepath), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
return JSON.stringify(data)
|
||||
}
|
28
tests/commands/update-readme.test.js
Normal file
28
tests/commands/update-readme.test.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmdirSync('tests/__data__/output', { recursive: true })
|
||||
fs.mkdirSync('tests/__data__/output')
|
||||
|
||||
execSync(
|
||||
'LOGS_DIR=tests/__data__/input/logs node scripts/commands/update-readme.js --config=tests/__data__/input/readme.json',
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
})
|
||||
|
||||
it('can update readme.md', () => {
|
||||
const output = content('tests/__data__/output/readme.md')
|
||||
const expected = content('tests/__data__/expected/readme.md')
|
||||
|
||||
expect(output).toBe(expected)
|
||||
})
|
||||
|
||||
function content(filepath) {
|
||||
const data = fs.readFileSync(path.resolve(filepath), {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
return JSON.stringify(data)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue